settings.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. 'use strict';
  2. // 系统设置:模型列表、配置读写(脱敏)、生图/对话 API 连通性测试
  3. const fs = require('fs');
  4. const path = require('path');
  5. const https = require('https');
  6. const { URL } = require('url');
  7. const { sendJson, readBody } = require('../util.js');
  8. const { DEFAULT_CHAT_MODEL } = require('../context.js');
  9. function register(app, ctx) {
  10. app.get('/api/models', async ({ res }) => {
  11. sendJson(res, 200, { models: ctx.models.MODELS, default: ctx.config.model });
  12. });
  13. app.get('/api/settings', async ({ res }) => {
  14. sendJson(res, 200, {
  15. apiKey: ctx.config.apiKey ? ctx.config.apiKey.slice(0, 8) + '••••••••' : '',
  16. apiBase: ctx.config.apiBase,
  17. model: ctx.config.model,
  18. rootDir: ctx.workspace.getWorkspaceRoot(),
  19. pageSize: ctx.config.pageSize,
  20. delayMs: ctx.config.delayMs,
  21. cookieConfigured: !!ctx.session.cookie && !!ctx.session.getKsid(),
  22. port: ctx.config.port,
  23. chatModel: ctx.config.chatModel || '',
  24. chatApiBase: ctx.config.chatApiBase || ctx.config.apiBase,
  25. chatApiKey: ctx.config.chatApiKey ? ctx.config.chatApiKey.slice(0, 8) + '••••••••' : '',
  26. chatKeyFallback: !ctx.config.chatApiKey && !!ctx.config.apiKey,
  27. });
  28. });
  29. app.post('/api/settings', async ({ req, res }) => {
  30. const body = await readBody(req);
  31. const config = ctx.config;
  32. const previousApiKey = config.apiKey;
  33. const previousCookie = ctx.session.cookie;
  34. if (body.apiKey && !body.apiKey.includes('••••')) config.apiKey = body.apiKey;
  35. if (body.model && ctx.models.MODEL_IDS.has(body.model)) config.model = body.model;
  36. if (body.rootDir) {
  37. const rootDir = path.resolve(String(body.rootDir).trim());
  38. if (!fs.existsSync(rootDir) || !fs.statSync(rootDir).isDirectory()) {
  39. return sendJson(res, 400, { error: '输出根目录不存在: ' + rootDir });
  40. }
  41. config.rootDir = rootDir;
  42. }
  43. if (body.pageSize !== undefined) {
  44. const pageSize = Number(body.pageSize);
  45. if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 200) {
  46. return sendJson(res, 400, { error: '每页条数必须为 1-200 的整数' });
  47. }
  48. config.pageSize = pageSize;
  49. }
  50. if (body.delayMs !== undefined) {
  51. const delayMs = Number(body.delayMs);
  52. if (!Number.isFinite(delayMs) || delayMs < 0 || delayMs > 60000) {
  53. return sendJson(res, 400, { error: '间隔必须为 0-60000 毫秒' });
  54. }
  55. config.delayMs = delayMs;
  56. }
  57. if (body.chatModel !== undefined) config.chatModel = String(body.chatModel).trim();
  58. if (body.chatApiBase !== undefined) config.chatApiBase = String(body.chatApiBase).trim();
  59. if (body.chatApiKey !== undefined && !String(body.chatApiKey).includes('••••')) config.chatApiKey = String(body.chatApiKey).trim();
  60. ctx.saveConfig();
  61. if (body.cookie !== undefined) {
  62. ctx.session.cookie = body.cookie;
  63. ctx.session.saveSession();
  64. }
  65. ctx.logs.pushSystemLog(
  66. `系统设置已保存:模型=${config.model},根目录=${config.rootDir},每页=${config.pageSize},间隔=${config.delayMs}ms` +
  67. (previousApiKey !== config.apiKey ? ',API Key 已更新' : '') +
  68. (body.cookie !== undefined && previousCookie !== ctx.session.cookie ? ',Cookie 已更新' : ''),
  69. 'success'
  70. );
  71. sendJson(res, 200, { ok: true });
  72. });
  73. // 生图 API 连通性测试:GET /v1/models(轻量、不产生生图费用)
  74. app.post('/api/image/test', async ({ res }) => {
  75. return new Promise((resolve) => {
  76. let base;
  77. try { base = new URL(ctx.config.apiBase); } catch { return sendJson(res, 400, { error: 'API 地址不合法: ' + ctx.config.apiBase }); }
  78. const options = {
  79. hostname: base.hostname, port: base.port || 443, path: '/v1/models', method: 'GET',
  80. headers: { Authorization: `Bearer ${ctx.config.apiKey}` }, timeout: 15000,
  81. };
  82. const up = https.request(options, (upRes) => {
  83. const chunks = [];
  84. upRes.on('data', (c) => chunks.push(c));
  85. upRes.on('end', () => {
  86. let json = null;
  87. try { json = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { /* 保留空 */ }
  88. const models = json && Array.isArray(json.data) ? json.data.map((m) => (m && m.id) || '').filter(Boolean) : null;
  89. sendJson(res, 200, { statusCode: upRes.statusCode, models, modelFound: !!(models && models.includes(ctx.config.model)), model: ctx.config.model });
  90. });
  91. });
  92. up.on('error', (e) => sendJson(res, 200, { statusCode: 0, error: e.message || String(e) }));
  93. up.on('timeout', () => up.destroy(new Error('请求超时')));
  94. up.end();
  95. });
  96. });
  97. app.post('/api/chat/test', async ({ req, res }) => {
  98. const body = await readBody(req);
  99. const nlQuery = require('../../nl-query.js');
  100. const cfg = {
  101. chatApiBase: body.chatApiBase || ctx.config.chatApiBase || ctx.config.apiBase,
  102. chatApiKey: (body.chatApiKey && !String(body.chatApiKey).includes('••••')) ? body.chatApiKey : (ctx.config.chatApiKey || ctx.config.apiKey),
  103. chatModel: body.chatModel || ctx.config.chatModel || DEFAULT_CHAT_MODEL,
  104. };
  105. try {
  106. const reply = await nlQuery.ping(cfg);
  107. sendJson(res, 200, { ok: true, reply });
  108. } catch (e) {
  109. sendJson(res, 502, { error: e.message });
  110. }
  111. });
  112. }
  113. module.exports = { register };