@leterhong/multichat 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +32 -0
- package/.agents/plugins/plugins/developer-toolkit/.codex-plugin/plugin.json +24 -0
- package/.agents/plugins/plugins/developer-toolkit/skills/code-review/SKILL.md +9 -0
- package/.agents/plugins/plugins/developer-toolkit/skills/code-review/agents/openai.yaml +3 -0
- package/.agents/plugins/plugins/developer-toolkit/skills/release-notes/SKILL.md +9 -0
- package/.agents/plugins/plugins/developer-toolkit/skills/release-notes/agents/openai.yaml +3 -0
- package/.agents/plugins/plugins/developer-toolkit/skills/security-review/SKILL.md +9 -0
- package/.agents/plugins/plugins/developer-toolkit/skills/security-review/agents/openai.yaml +3 -0
- package/.agents/plugins/plugins/developer-toolkit/skills/test-plan/SKILL.md +9 -0
- package/.agents/plugins/plugins/developer-toolkit/skills/test-plan/agents/openai.yaml +3 -0
- package/.agents/plugins/plugins/writing-toolkit/.codex-plugin/plugin.json +24 -0
- package/.agents/plugins/plugins/writing-toolkit/skills/meeting-notes/SKILL.md +9 -0
- package/.agents/plugins/plugins/writing-toolkit/skills/meeting-notes/agents/openai.yaml +3 -0
- package/.agents/plugins/plugins/writing-toolkit/skills/resume-tune/SKILL.md +9 -0
- package/.agents/plugins/plugins/writing-toolkit/skills/resume-tune/agents/openai.yaml +3 -0
- package/.agents/plugins/plugins/writing-toolkit/skills/weekly-report/SKILL.md +9 -0
- package/.agents/plugins/plugins/writing-toolkit/skills/weekly-report/agents/openai.yaml +3 -0
- package/.agents/skills/.gitkeep +1 -0
- package/.agents/skills/wanghong-handwritten-ppt/SKILL.md +172 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/HTML-PPT-LICENSE +21 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/NEAT-ANNOTATIONS-LICENSE +21 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/animations.css +138 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/base.css +150 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/deepseek-example.css +893 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/neat-annotations.css +152 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/preview-cover.png +0 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/preview-diagram.png +0 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/preview-summary.png +0 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/preview-table.png +0 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/runtime.js +960 -0
- package/.agents/skills/wanghong-handwritten-ppt/assets/template.css +198 -0
- package/.agents/skills/wanghong-handwritten-ppt/evals/evals.json +20 -0
- package/.agents/skills/wanghong-handwritten-ppt/examples/deepseek-v4-flash/index.html +414 -0
- package/.agents/skills/wanghong-handwritten-ppt/examples/deepseek-v4-flash/style.css +897 -0
- package/.agents/skills/wanghong-handwritten-ppt/references/prompts.md +170 -0
- package/.agents/skills/wanghong-handwritten-ppt/references/style-guide.md +133 -0
- package/.agents/skills/wanghong-handwritten-ppt/scripts/check_deck.py +41 -0
- package/.agents/skills/wanghong-handwritten-ppt/scripts/prepare_render_html.py +137 -0
- package/.agents/skills/wanghong-handwritten-ppt/scripts/render.sh +110 -0
- package/.agents/skills/wanghong-handwritten-ppt/templates/deck.html +94 -0
- package/.agents/skills/wanghong-handwritten-ppt/tests/test_font_contract.py +73 -0
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/backend/adapters/README.md +77 -0
- package/backend/adapters/anthropic.ts +115 -0
- package/backend/adapters/base.ts +47 -0
- package/backend/adapters/index.ts +41 -0
- package/backend/adapters/lmstudio.ts +7 -0
- package/backend/adapters/moonshot.ts +7 -0
- package/backend/adapters/ollama.ts +7 -0
- package/backend/adapters/openai-compatible.ts +51 -0
- package/backend/adapters/openai.ts +7 -0
- package/backend/adapters/qwen.ts +112 -0
- package/backend/adapters/wenxin.ts +136 -0
- package/backend/adapters/zhipu.ts +93 -0
- package/backend/dist/adapters/anthropic.js +130 -0
- package/backend/dist/adapters/anthropic.js.map +1 -0
- package/backend/dist/adapters/base.js +38 -0
- package/backend/dist/adapters/base.js.map +1 -0
- package/backend/dist/adapters/index.js +39 -0
- package/backend/dist/adapters/index.js.map +1 -0
- package/backend/dist/adapters/lmstudio.js +8 -0
- package/backend/dist/adapters/lmstudio.js.map +1 -0
- package/backend/dist/adapters/moonshot.js +8 -0
- package/backend/dist/adapters/moonshot.js.map +1 -0
- package/backend/dist/adapters/ollama.js +8 -0
- package/backend/dist/adapters/ollama.js.map +1 -0
- package/backend/dist/adapters/openai-compatible.js +47 -0
- package/backend/dist/adapters/openai-compatible.js.map +1 -0
- package/backend/dist/adapters/openai.js +8 -0
- package/backend/dist/adapters/openai.js.map +1 -0
- package/backend/dist/adapters/qwen.js +103 -0
- package/backend/dist/adapters/qwen.js.map +1 -0
- package/backend/dist/adapters/wenxin.js +114 -0
- package/backend/dist/adapters/wenxin.js.map +1 -0
- package/backend/dist/adapters/zhipu.js +86 -0
- package/backend/dist/adapters/zhipu.js.map +1 -0
- package/backend/dist/extensions/demo-mcp-server.js +75 -0
- package/backend/dist/extensions/demo-mcp-server.js.map +1 -0
- package/backend/dist/extensions/importer.js +828 -0
- package/backend/dist/extensions/importer.js.map +1 -0
- package/backend/dist/extensions/manager.js +1154 -0
- package/backend/dist/extensions/manager.js.map +1 -0
- package/backend/dist/lib/catalog.js +30 -0
- package/backend/dist/lib/catalog.js.map +1 -0
- package/backend/dist/lib/context.js +59 -0
- package/backend/dist/lib/context.js.map +1 -0
- package/backend/dist/lib/errors.js +75 -0
- package/backend/dist/lib/errors.js.map +1 -0
- package/backend/dist/lib/provider-store.js +116 -0
- package/backend/dist/lib/provider-store.js.map +1 -0
- package/backend/dist/lib/redact.js +11 -0
- package/backend/dist/lib/redact.js.map +1 -0
- package/backend/dist/lib/runtime-store.js +28 -0
- package/backend/dist/lib/runtime-store.js.map +1 -0
- package/backend/dist/lib/sqlite-store.js +218 -0
- package/backend/dist/lib/sqlite-store.js.map +1 -0
- package/backend/dist/lib/sse.js +46 -0
- package/backend/dist/lib/sse.js.map +1 -0
- package/backend/dist/lib/ssrf.js +131 -0
- package/backend/dist/lib/ssrf.js.map +1 -0
- package/backend/dist/lib/store.js +84 -0
- package/backend/dist/lib/store.js.map +1 -0
- package/backend/dist/lib/usage.js +219 -0
- package/backend/dist/lib/usage.js.map +1 -0
- package/backend/dist/lib/util.js +38 -0
- package/backend/dist/lib/util.js.map +1 -0
- package/backend/dist/lib/workspace-store.js +152 -0
- package/backend/dist/lib/workspace-store.js.map +1 -0
- package/backend/dist/mcp.js +429 -0
- package/backend/dist/mcp.js.map +1 -0
- package/backend/dist/routes/agents.js +51 -0
- package/backend/dist/routes/agents.js.map +1 -0
- package/backend/dist/routes/assistants.js +32 -0
- package/backend/dist/routes/assistants.js.map +1 -0
- package/backend/dist/routes/chat.js +697 -0
- package/backend/dist/routes/chat.js.map +1 -0
- package/backend/dist/routes/control-plane.js +200 -0
- package/backend/dist/routes/control-plane.js.map +1 -0
- package/backend/dist/routes/conversations.js +66 -0
- package/backend/dist/routes/conversations.js.map +1 -0
- package/backend/dist/routes/extensions-import.js +22 -0
- package/backend/dist/routes/extensions-import.js.map +1 -0
- package/backend/dist/routes/import.js +85 -0
- package/backend/dist/routes/import.js.map +1 -0
- package/backend/dist/routes/mcp-servers.js +56 -0
- package/backend/dist/routes/mcp-servers.js.map +1 -0
- package/backend/dist/routes/meta.js +37 -0
- package/backend/dist/routes/meta.js.map +1 -0
- package/backend/dist/routes/mock.js +28 -0
- package/backend/dist/routes/mock.js.map +1 -0
- package/backend/dist/routes/plugins.js +41 -0
- package/backend/dist/routes/plugins.js.map +1 -0
- package/backend/dist/routes/providers.js +197 -0
- package/backend/dist/routes/providers.js.map +1 -0
- package/backend/dist/routes/runs.js +57 -0
- package/backend/dist/routes/runs.js.map +1 -0
- package/backend/dist/routes/skills.js +73 -0
- package/backend/dist/routes/skills.js.map +1 -0
- package/backend/dist/routes/tools.js +40 -0
- package/backend/dist/routes/tools.js.map +1 -0
- package/backend/dist/routes/usage.js +14 -0
- package/backend/dist/routes/usage.js.map +1 -0
- package/backend/dist/routes/workspaces.js +158 -0
- package/backend/dist/routes/workspaces.js.map +1 -0
- package/backend/dist/runtime/agent.js +518 -0
- package/backend/dist/runtime/agent.js.map +1 -0
- package/backend/dist/server.js +103 -0
- package/backend/dist/server.js.map +1 -0
- package/backend/dist/types.js +3 -0
- package/backend/dist/types.js.map +1 -0
- package/backend/extensions/demo-mcp-server.js +70 -0
- package/backend/extensions/importer.ts +709 -0
- package/backend/extensions/manager.ts +1055 -0
- package/backend/lib/catalog.ts +28 -0
- package/backend/lib/context.ts +61 -0
- package/backend/lib/errors.ts +78 -0
- package/backend/lib/provider-store.ts +116 -0
- package/backend/lib/redact.ts +9 -0
- package/backend/lib/runtime-store.ts +38 -0
- package/backend/lib/sqlite-store.ts +220 -0
- package/backend/lib/sse.ts +42 -0
- package/backend/lib/ssrf.ts +120 -0
- package/backend/lib/store.ts +79 -0
- package/backend/lib/usage.ts +212 -0
- package/backend/lib/util.ts +31 -0
- package/backend/lib/workspace-store.ts +150 -0
- package/backend/mcp.ts +417 -0
- package/backend/package.json +34 -0
- package/backend/prompts.json +82 -0
- package/backend/routes/agents.ts +55 -0
- package/backend/routes/assistants.ts +30 -0
- package/backend/routes/chat.ts +660 -0
- package/backend/routes/control-plane.ts +165 -0
- package/backend/routes/conversations.ts +62 -0
- package/backend/routes/extensions-import.ts +15 -0
- package/backend/routes/import.ts +72 -0
- package/backend/routes/mcp-servers.ts +36 -0
- package/backend/routes/meta.ts +38 -0
- package/backend/routes/mock.ts +25 -0
- package/backend/routes/plugins.ts +27 -0
- package/backend/routes/providers.ts +166 -0
- package/backend/routes/runs.ts +55 -0
- package/backend/routes/skills.ts +52 -0
- package/backend/routes/tools.ts +40 -0
- package/backend/routes/usage.ts +13 -0
- package/backend/routes/workspaces.ts +124 -0
- package/backend/runtime/agent.ts +474 -0
- package/backend/server.ts +106 -0
- package/backend/types.ts +126 -0
- package/bin/multichat.mjs +535 -0
- package/frontend/dist/assets/agentPicker-C39VzmYa.js +13 -0
- package/frontend/dist/assets/compare-BpaGD32J.js +1 -0
- package/frontend/dist/assets/conversations-COuLmg99.js +7 -0
- package/frontend/dist/assets/data-ZH7Dux1K.js +1 -0
- package/frontend/dist/assets/extensionImport-CnTWKtWm.js +28 -0
- package/frontend/dist/assets/importExport-B3mB-9zs.js +70 -0
- package/frontend/dist/assets/index-CASVCOUO.js +34 -0
- package/frontend/dist/assets/index-DSvbjNLc.css +1 -0
- package/frontend/dist/assets/init-ZPK483vB.js +1 -0
- package/frontend/dist/assets/jsx-runtime-ZSsbr0oW.js +9 -0
- package/frontend/dist/assets/modal-CgHocv3n.js +2 -0
- package/frontend/dist/assets/modelPicker-EUmmVnbj.js +14 -0
- package/frontend/dist/assets/pluginsUI-B6jGUQkx.js +16 -0
- package/frontend/dist/assets/render-DgAg1K8A.js +1 -0
- package/frontend/dist/assets/send-BJhZ_5YA.js +14 -0
- package/frontend/dist/assets/settings-DdnYwZl-.js +2 -0
- package/frontend/dist/assets/shell-DFV2khQl.js +1 -0
- package/frontend/dist/assets/workbench-BY2J4vWC.js +41 -0
- package/frontend/dist/icon-maskable.svg +4 -0
- package/frontend/dist/icon.svg +4 -0
- package/frontend/dist/index.html +21 -0
- package/frontend/dist/manifest.webmanifest +26 -0
- package/package.json +58 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function readPackageVersion() {
|
|
5
|
+
const candidates = [
|
|
6
|
+
path.join(__dirname, '..', 'package.json'),
|
|
7
|
+
path.join(__dirname, '..', '..', 'package.json'),
|
|
8
|
+
];
|
|
9
|
+
for (const file of candidates) {
|
|
10
|
+
try {
|
|
11
|
+
const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
12
|
+
if (pkg.version) return pkg.version;
|
|
13
|
+
} catch {
|
|
14
|
+
// Source and compiled builds have different __dirname depths.
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return '0.0.0';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function safeId(value, label = 'id') {
|
|
21
|
+
const id = String(value || '').trim();
|
|
22
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(id)) {
|
|
23
|
+
throw new Error(`invalid ${label}`);
|
|
24
|
+
}
|
|
25
|
+
return id;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { readPackageVersion, safeId };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// ── 共享上下文(单例):集中依赖与常量,避免每个路由重复 require / 重复定义 ──
|
|
3
|
+
// 注意:本模块在 require 时构造 store / workspaceStore,均为本地 IO,不启动服务,
|
|
4
|
+
// 因此可被 routes / services / plugins 各层安全依赖,无循环依赖。
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { getMcpClient, closeMcpClient, closeAllMcpClients } = require('../mcp');
|
|
8
|
+
const { createRuntimeStore } = require('./runtime-store');
|
|
9
|
+
const { createWorkspaceStore } = require('./workspace-store');
|
|
10
|
+
const { createProviderStore } = require('./provider-store');
|
|
11
|
+
const { safeFetch } = require('./ssrf');
|
|
12
|
+
const { createAdapter, ADAPTER_MAP } = require('../adapters');
|
|
13
|
+
const { readPackageVersion, safeId } = require('./catalog');
|
|
14
|
+
|
|
15
|
+
const PORT = process.env.PORT || 3000;
|
|
16
|
+
const BACKEND_ROOT = path.basename(__dirname) === 'lib' && path.basename(path.dirname(__dirname)) === 'dist'
|
|
17
|
+
? path.dirname(path.dirname(__dirname))
|
|
18
|
+
: path.dirname(__dirname);
|
|
19
|
+
const DATA_DIR = process.env.DATA_DIR || path.join(BACKEND_ROOT, 'data');
|
|
20
|
+
const runtimeStore = createRuntimeStore(DATA_DIR);
|
|
21
|
+
const store = runtimeStore.store;
|
|
22
|
+
const STORAGE_FALLBACK = runtimeStore.fallbackReason ? 'json' : null;
|
|
23
|
+
const workspaceStore = createWorkspaceStore(store);
|
|
24
|
+
const providerStore = createProviderStore(store);
|
|
25
|
+
const VERSION = readPackageVersion();
|
|
26
|
+
|
|
27
|
+
// 插件 / 技能 / 智能体 / 运行 相关常量(原 server.ts 顶层常量集中于此)
|
|
28
|
+
const SKILL_FILE = 'skills.json';
|
|
29
|
+
const AGENT_FILE = 'agents.json';
|
|
30
|
+
const RUN_FILE = 'runs.json';
|
|
31
|
+
const MAX_RUNS = 200;
|
|
32
|
+
|
|
33
|
+
// 运行中的 Agent 任务取消句柄:runId → AbortController。
|
|
34
|
+
// 用于「Cancellation」:前端停止时,后端能真正中断上游 fetch 与 MCP 子进程。
|
|
35
|
+
const runAborts = new Map();
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
fs,
|
|
39
|
+
path,
|
|
40
|
+
getMcpClient,
|
|
41
|
+
closeMcpClient,
|
|
42
|
+
closeAllMcpClients,
|
|
43
|
+
BACKEND_ROOT,
|
|
44
|
+
store,
|
|
45
|
+
workspaceStore,
|
|
46
|
+
providerStore,
|
|
47
|
+
safeFetch,
|
|
48
|
+
createAdapter,
|
|
49
|
+
ADAPTER_MAP,
|
|
50
|
+
readPackageVersion,
|
|
51
|
+
safeId,
|
|
52
|
+
PORT,
|
|
53
|
+
DATA_DIR,
|
|
54
|
+
STORAGE_FALLBACK,
|
|
55
|
+
VERSION,
|
|
56
|
+
SKILL_FILE,
|
|
57
|
+
AGENT_FILE,
|
|
58
|
+
RUN_FILE,
|
|
59
|
+
MAX_RUNS,
|
|
60
|
+
runAborts,
|
|
61
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// ── 统一错误码 + 请求ID 中间件(A3) ───────────────────────────────────
|
|
2
|
+
// 设计目标:在不破坏既有契约(前端读 res.data.error 字符串)的前提下,
|
|
3
|
+
// 为每次请求附加 requestId,并为错误响应附加 code 字段,便于排障与日志关联。
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
6
|
+
const { redactSecrets } = require('./redact');
|
|
7
|
+
|
|
8
|
+
// 业务错误码 → HTTP 状态码 映射
|
|
9
|
+
const ERROR_CODES: Record<string, number> = {
|
|
10
|
+
BAD_REQUEST: 400,
|
|
11
|
+
UNAUTHORIZED: 401,
|
|
12
|
+
FORBIDDEN: 403,
|
|
13
|
+
NOT_FOUND: 404,
|
|
14
|
+
VALIDATION: 400,
|
|
15
|
+
IMPORT_FAILED: 502,
|
|
16
|
+
SSRF_BLOCKED: 403,
|
|
17
|
+
PLUGIN_NOT_FOUND: 404,
|
|
18
|
+
PROVIDER_NOT_FOUND: 404,
|
|
19
|
+
AGENT_NOT_FOUND: 404,
|
|
20
|
+
SKILL_NOT_FOUND: 404,
|
|
21
|
+
PERMISSION_DENIED: 403,
|
|
22
|
+
APPROVAL_NOT_PENDING: 409,
|
|
23
|
+
CONFLICT: 409,
|
|
24
|
+
INVALID_PACKAGE: 400,
|
|
25
|
+
MCP_CONNECTION_FAILED: 502,
|
|
26
|
+
UPSTREAM: 502,
|
|
27
|
+
INTERNAL: 500,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
class AppError extends Error {
|
|
31
|
+
code: string;
|
|
32
|
+
statusCode: number;
|
|
33
|
+
constructor(code: string, message: string, statusCode?: number) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = 'AppError';
|
|
36
|
+
this.code = ERROR_CODES[code] !== undefined ? code : 'INTERNAL';
|
|
37
|
+
this.statusCode = statusCode || ERROR_CODES[this.code] || 500;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 请求ID 中间件:每个请求分配一个 id(允许上游透传 X-Request-Id)
|
|
42
|
+
function requestIdMiddleware(req: Request, res: Response, next: NextFunction): void {
|
|
43
|
+
const supplied = typeof req.headers['x-request-id'] === 'string' ? req.headers['x-request-id'] : '';
|
|
44
|
+
const id = /^[A-Za-z0-9._:-]{1,128}$/.test(supplied)
|
|
45
|
+
? supplied
|
|
46
|
+
: crypto.randomBytes(8).toString('hex');
|
|
47
|
+
(req as any).requestId = id;
|
|
48
|
+
res.locals.requestId = id;
|
|
49
|
+
res.setHeader('X-Request-Id', id);
|
|
50
|
+
next();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 结构化失败响应:保持 error 为字符串以兼容旧前端,额外带 code / requestId
|
|
54
|
+
function fail(res: Response, status: number, code: string, message: string) {
|
|
55
|
+
return res.status(status).json({
|
|
56
|
+
error: message,
|
|
57
|
+
code,
|
|
58
|
+
requestId: res.locals.requestId || null,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 全局错误处理:兜底未捕获异常,统一输出 { error, code, requestId }
|
|
63
|
+
function errorHandler(err: any, req: Request, res: Response, next: NextFunction): void {
|
|
64
|
+
if (res.headersSent) return next(err);
|
|
65
|
+
const status = err.statusCode || 500;
|
|
66
|
+
const code = err.code || 'INTERNAL';
|
|
67
|
+
const message = redactSecrets(err.message || 'Internal Server Error');
|
|
68
|
+
if (status >= 500) {
|
|
69
|
+
console.error('[error]', code, redactSecrets(message), 'reqId=', (req as any).requestId, err.stack ? '\n' + redactSecrets(err.stack) : '');
|
|
70
|
+
}
|
|
71
|
+
res.status(status).json({
|
|
72
|
+
error: message,
|
|
73
|
+
code,
|
|
74
|
+
requestId: (req as any).requestId || res.locals.requestId || null,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { ERROR_CODES, AppError, requestIdMiddleware, fail, errorHandler };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
import type { JsonRecord, JsonStore } from '../types';
|
|
4
|
+
|
|
5
|
+
const PROVIDER_FILE = 'providers.json';
|
|
6
|
+
const KEY_FILE = '.provider-secrets.key';
|
|
7
|
+
const PREFIX = 'enc:v1:';
|
|
8
|
+
|
|
9
|
+
function protectFile(file: string) {
|
|
10
|
+
try { fs.chmodSync(file, 0o600); } catch {}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function keyFor(store: JsonStore): Buffer {
|
|
14
|
+
const file = store.resolve(KEY_FILE);
|
|
15
|
+
if (!fs.existsSync(file)) {
|
|
16
|
+
fs.writeFileSync(file, crypto.randomBytes(32), { mode: 0o600 });
|
|
17
|
+
}
|
|
18
|
+
const key = fs.readFileSync(file);
|
|
19
|
+
if (key.length !== 32) throw new Error('provider secret key is invalid');
|
|
20
|
+
protectFile(file);
|
|
21
|
+
return key;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function encrypt(store: JsonStore, value: string): string {
|
|
25
|
+
if (!value || value.startsWith(PREFIX)) return value;
|
|
26
|
+
const iv = crypto.randomBytes(12);
|
|
27
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', keyFor(store), iv);
|
|
28
|
+
const encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
|
|
29
|
+
const tag = cipher.getAuthTag();
|
|
30
|
+
return PREFIX + [iv, tag, encrypted].map(part => part.toString('base64url')).join(':');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function decrypt(store: JsonStore, value: string): string {
|
|
34
|
+
if (!value || !value.startsWith(PREFIX)) return value || '';
|
|
35
|
+
const [ivText, tagText, payloadText] = value.slice(PREFIX.length).split(':');
|
|
36
|
+
if (!ivText || !tagText || !payloadText) throw new Error('provider secret payload is invalid');
|
|
37
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', keyFor(store), Buffer.from(ivText, 'base64url'));
|
|
38
|
+
decipher.setAuthTag(Buffer.from(tagText, 'base64url'));
|
|
39
|
+
return Buffer.concat([decipher.update(Buffer.from(payloadText, 'base64url')), decipher.final()]).toString('utf8');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createProviderStore(store: JsonStore) {
|
|
43
|
+
function stored(): JsonRecord[] {
|
|
44
|
+
const rows = store.read<JsonRecord[]>(PROVIDER_FILE, []);
|
|
45
|
+
let migrated = false;
|
|
46
|
+
const protectedRows = rows.map(row => {
|
|
47
|
+
if (row.apiKey && !String(row.apiKey).startsWith(PREFIX)) {
|
|
48
|
+
migrated = true;
|
|
49
|
+
return { ...row, apiKey: encrypt(store, String(row.apiKey)) };
|
|
50
|
+
}
|
|
51
|
+
return row;
|
|
52
|
+
});
|
|
53
|
+
if (migrated) {
|
|
54
|
+
store.write(PROVIDER_FILE, protectedRows);
|
|
55
|
+
// The generic store created its recovery copy from the legacy plaintext
|
|
56
|
+
// file. Replace that copy immediately so no API key remains in a .bak.
|
|
57
|
+
const legacyFile = store.resolve(PROVIDER_FILE);
|
|
58
|
+
if (store.kind === 'json' || fs.existsSync(legacyFile)) {
|
|
59
|
+
fs.writeFileSync(legacyFile, JSON.stringify(protectedRows, null, 2), { mode: 0o600 });
|
|
60
|
+
fs.writeFileSync(`${legacyFile}.bak`, JSON.stringify(protectedRows, null, 2), { mode: 0o600 });
|
|
61
|
+
protectFile(legacyFile);
|
|
62
|
+
protectFile(`${legacyFile}.bak`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return protectedRows;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function list(): JsonRecord[] {
|
|
69
|
+
return stored().map(row => {
|
|
70
|
+
const apiKey = row.apiKey ? decrypt(store, String(row.apiKey)) : '';
|
|
71
|
+
return { ...row, apiKey };
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function save(rows: JsonRecord[]) {
|
|
76
|
+
const protectedRows = rows.map(row => ({
|
|
77
|
+
...row,
|
|
78
|
+
apiKey: row.apiKey ? encrypt(store, String(row.apiKey)) : '',
|
|
79
|
+
}));
|
|
80
|
+
store.write(PROVIDER_FILE, protectedRows);
|
|
81
|
+
if (store.kind === 'json') protectFile(store.resolve(PROVIDER_FILE));
|
|
82
|
+
const backup = `${store.resolve(PROVIDER_FILE)}.bak`;
|
|
83
|
+
if (fs.existsSync(backup)) protectFile(backup);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function publicRecord(row: JsonRecord) {
|
|
87
|
+
if (!row) return row;
|
|
88
|
+
const { apiKey, ...rest } = row;
|
|
89
|
+
const capabilities = providerCapabilities(row);
|
|
90
|
+
if (!apiKey) return { ...rest, capabilities };
|
|
91
|
+
const preview = String(apiKey).length > 4 ? String(apiKey).slice(-4) : String(apiKey);
|
|
92
|
+
return { ...rest, capabilities, apiKeyMasked: true, apiKeyPreview: preview };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function publicList() { return list().map(publicRecord); }
|
|
96
|
+
|
|
97
|
+
return { list, save, publicRecord, publicList };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function providerCapabilities(provider: JsonRecord) {
|
|
101
|
+
const type = String(provider.apiType || 'openai').toLowerCase();
|
|
102
|
+
const models = Array.isArray(provider.models) ? provider.models.map(String) : [];
|
|
103
|
+
const modelText = models.join(' ').toLowerCase();
|
|
104
|
+
const local = ['ollama', 'lmstudio'].includes(type);
|
|
105
|
+
return {
|
|
106
|
+
streaming: true,
|
|
107
|
+
tools: !['wenxin'].includes(type),
|
|
108
|
+
vision: /vision|vl|gpt-4o|gemini|claude-3/.test(modelText),
|
|
109
|
+
reasoning: /reason|r1|thinking|qwq/.test(modelText),
|
|
110
|
+
usage: 'provider-or-estimated',
|
|
111
|
+
local,
|
|
112
|
+
privateNetwork: Boolean(provider.allowPrivate) || local,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { PROVIDER_FILE, createProviderStore, providerCapabilities };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
function redactSecrets(value: unknown): string {
|
|
2
|
+
return String(value ?? '')
|
|
3
|
+
.replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+\/-]{8,}={0,2}/gi, '$1 ***')
|
|
4
|
+
.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, 'sk-***')
|
|
5
|
+
.replace(/([?&](?:access_token|client_secret|api_?key|token|password)=)[^&#\s]+/gi, '$1***')
|
|
6
|
+
.replace(/\b((?:api_?key|access_?token|client_?secret|authorization|password)\s*[:=]\s*)[^\s,;]+/gi, '$1***');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
module.exports = { redactSecrets };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import type { JsonStore } from '../types';
|
|
4
|
+
|
|
5
|
+
type StoreFactory = (rootDir: string) => JsonStore;
|
|
6
|
+
type RuntimeStoreOptions = {
|
|
7
|
+
preferred?: 'sqlite' | 'json';
|
|
8
|
+
createJsonStore?: StoreFactory;
|
|
9
|
+
createSqliteStore?: StoreFactory;
|
|
10
|
+
warn?: (message: string) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function errorMessage(error: unknown) {
|
|
14
|
+
return error instanceof Error ? error.message : String(error);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Selects the local persistence backend without making SQLite a hard startup
|
|
19
|
+
* dependency. The SQLite module is loaded lazily so older or restricted Node
|
|
20
|
+
* runtimes can still reach the JSON recovery path.
|
|
21
|
+
*/
|
|
22
|
+
function createRuntimeStore(rootDir: string, options: RuntimeStoreOptions = {}) {
|
|
23
|
+
const preferred = options.preferred || (process.env.MULTICHAT_STORE === 'json' ? 'json' : 'sqlite');
|
|
24
|
+
const createJson = options.createJsonStore || (require('./store').createJsonStore as StoreFactory);
|
|
25
|
+
if (preferred === 'json') return { store: createJson(rootDir), fallbackReason: null };
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const createSqlite = options.createSqliteStore || (require('./sqlite-store').createSqliteStore as StoreFactory);
|
|
29
|
+
return { store: createSqlite(rootDir), fallbackReason: null };
|
|
30
|
+
} catch (error) {
|
|
31
|
+
const fallbackReason = errorMessage(error);
|
|
32
|
+
const message = `[MultiChat] SQLite 初始化失败,已降级为 JSON 存储:${fallbackReason}`;
|
|
33
|
+
(options.warn || console.warn)(message);
|
|
34
|
+
return { store: createJson(rootDir), fallbackReason };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { createRuntimeStore };
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
4
|
+
import type { JsonRecord, JsonStore } from '../types';
|
|
5
|
+
|
|
6
|
+
type SearchRow = {
|
|
7
|
+
entityId: string;
|
|
8
|
+
projectId: string;
|
|
9
|
+
title: string;
|
|
10
|
+
snippet: string;
|
|
11
|
+
rank: number;
|
|
12
|
+
source: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function createSqliteStore(rootDir: string): JsonStore {
|
|
16
|
+
fs.mkdirSync(rootDir, { recursive: true });
|
|
17
|
+
const databaseFile = path.join(rootDir, 'multichat.sqlite3');
|
|
18
|
+
const database = new DatabaseSync(databaseFile);
|
|
19
|
+
const chains = new Map<string, Promise<unknown>>();
|
|
20
|
+
|
|
21
|
+
database.exec(`
|
|
22
|
+
PRAGMA journal_mode = WAL;
|
|
23
|
+
PRAGMA synchronous = NORMAL;
|
|
24
|
+
PRAGMA busy_timeout = 5000;
|
|
25
|
+
CREATE TABLE IF NOT EXISTS kv_store (
|
|
26
|
+
name TEXT PRIMARY KEY,
|
|
27
|
+
value TEXT NOT NULL,
|
|
28
|
+
updated_at TEXT NOT NULL
|
|
29
|
+
);
|
|
30
|
+
CREATE TABLE IF NOT EXISTS store_meta (
|
|
31
|
+
key TEXT PRIMARY KEY,
|
|
32
|
+
value TEXT NOT NULL
|
|
33
|
+
);
|
|
34
|
+
`);
|
|
35
|
+
|
|
36
|
+
let ftsEnabled = false;
|
|
37
|
+
try {
|
|
38
|
+
database.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS search_fts USING fts5(
|
|
39
|
+
source UNINDEXED,
|
|
40
|
+
entity_id UNINDEXED,
|
|
41
|
+
project_id UNINDEXED,
|
|
42
|
+
title,
|
|
43
|
+
content,
|
|
44
|
+
tokenize='trigram'
|
|
45
|
+
);`);
|
|
46
|
+
ftsEnabled = true;
|
|
47
|
+
} catch {
|
|
48
|
+
try {
|
|
49
|
+
database.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS search_fts USING fts5(
|
|
50
|
+
source UNINDEXED,
|
|
51
|
+
entity_id UNINDEXED,
|
|
52
|
+
project_id UNINDEXED,
|
|
53
|
+
title,
|
|
54
|
+
content
|
|
55
|
+
);`);
|
|
56
|
+
ftsEnabled = true;
|
|
57
|
+
} catch {
|
|
58
|
+
database.exec(`
|
|
59
|
+
CREATE TABLE IF NOT EXISTS search_documents (
|
|
60
|
+
source TEXT NOT NULL,
|
|
61
|
+
entity_id TEXT NOT NULL,
|
|
62
|
+
project_id TEXT NOT NULL,
|
|
63
|
+
title TEXT NOT NULL,
|
|
64
|
+
content TEXT NOT NULL,
|
|
65
|
+
PRIMARY KEY(source, entity_id)
|
|
66
|
+
);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS search_documents_project ON search_documents(project_id);
|
|
68
|
+
`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const selectValue = database.prepare('SELECT value FROM kv_store WHERE name = ?');
|
|
73
|
+
const upsertValue = database.prepare(`
|
|
74
|
+
INSERT INTO kv_store(name, value, updated_at) VALUES (?, ?, ?)
|
|
75
|
+
ON CONFLICT(name) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
|
76
|
+
`);
|
|
77
|
+
const deleteValue = database.prepare('DELETE FROM kv_store WHERE name = ?');
|
|
78
|
+
const migrationValue = database.prepare("SELECT value FROM store_meta WHERE key = 'json_migration_v1'");
|
|
79
|
+
|
|
80
|
+
function validName(name: string) {
|
|
81
|
+
if (typeof name !== 'string' || !name || path.basename(name) !== name) throw new Error('invalid data file name');
|
|
82
|
+
return name;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolve(name: string) {
|
|
86
|
+
return path.join(rootDir, validName(name));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function importJsonFiles() {
|
|
90
|
+
if (migrationValue.get()) return;
|
|
91
|
+
database.exec('BEGIN IMMEDIATE');
|
|
92
|
+
try {
|
|
93
|
+
const names = fs.readdirSync(rootDir).filter((name: string) => name.endsWith('.json') && path.basename(name) === name);
|
|
94
|
+
for (const name of names) {
|
|
95
|
+
if (selectValue.get(name)) continue;
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(rootDir, name), 'utf8'));
|
|
98
|
+
upsertValue.run(name, JSON.stringify(parsed), new Date().toISOString());
|
|
99
|
+
} catch {
|
|
100
|
+
// Corrupt legacy files remain untouched for manual recovery.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
database.prepare("INSERT OR REPLACE INTO store_meta(key, value) VALUES ('json_migration_v1', ?)").run(new Date().toISOString());
|
|
104
|
+
database.exec('COMMIT');
|
|
105
|
+
} catch (error) {
|
|
106
|
+
database.exec('ROLLBACK');
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function read<T>(name: string, fallback: T): T {
|
|
112
|
+
validName(name);
|
|
113
|
+
const row = selectValue.get(name) as { value?: string } | undefined;
|
|
114
|
+
if (!row?.value) return fallback;
|
|
115
|
+
try { return JSON.parse(row.value) as T; }
|
|
116
|
+
catch { return fallback; }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function reindex(source: string, value: unknown) {
|
|
120
|
+
if (!['assets.json', 'memories.json'].includes(source)) return;
|
|
121
|
+
const rows = Array.isArray(value) ? value : [];
|
|
122
|
+
const table = ftsEnabled ? 'search_fts' : 'search_documents';
|
|
123
|
+
const removeSource = database.prepare(`DELETE FROM ${table} WHERE source = ?`);
|
|
124
|
+
const insert = database.prepare(`INSERT INTO ${table}(source, entity_id, project_id, title, content) VALUES (?, ?, ?, ?, ?)`);
|
|
125
|
+
database.exec('SAVEPOINT reindex_source');
|
|
126
|
+
try {
|
|
127
|
+
removeSource.run(source);
|
|
128
|
+
for (const row of rows as JsonRecord[]) {
|
|
129
|
+
if (!row?.id || !row?.content) continue;
|
|
130
|
+
insert.run(source, String(row.id), String(row.projectId || ''), String(row.name || row.title || ''), String(row.content));
|
|
131
|
+
}
|
|
132
|
+
database.exec('RELEASE reindex_source');
|
|
133
|
+
} catch (error) {
|
|
134
|
+
database.exec('ROLLBACK TO reindex_source');
|
|
135
|
+
database.exec('RELEASE reindex_source');
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function write(name: string, value: unknown) {
|
|
141
|
+
validName(name);
|
|
142
|
+
database.exec('BEGIN IMMEDIATE');
|
|
143
|
+
try {
|
|
144
|
+
upsertValue.run(name, JSON.stringify(value), new Date().toISOString());
|
|
145
|
+
reindex(name, value);
|
|
146
|
+
database.exec('COMMIT');
|
|
147
|
+
} catch (error) {
|
|
148
|
+
database.exec('ROLLBACK');
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function remove(name: string) {
|
|
154
|
+
validName(name);
|
|
155
|
+
database.exec('BEGIN IMMEDIATE');
|
|
156
|
+
try {
|
|
157
|
+
deleteValue.run(name);
|
|
158
|
+
database.prepare(`DELETE FROM ${ftsEnabled ? 'search_fts' : 'search_documents'} WHERE source = ?`).run(name);
|
|
159
|
+
database.exec('COMMIT');
|
|
160
|
+
} catch (error) {
|
|
161
|
+
database.exec('ROLLBACK');
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function mutate<T>(name: string, update: (current: T) => T | undefined | Promise<T | undefined>, fallback: T) {
|
|
167
|
+
const previous = chains.get(name) || Promise.resolve();
|
|
168
|
+
const run = previous
|
|
169
|
+
.then(() => update(read(name, fallback)))
|
|
170
|
+
.then((result) => {
|
|
171
|
+
if (result !== undefined) write(name, result);
|
|
172
|
+
return result;
|
|
173
|
+
});
|
|
174
|
+
chains.set(name, run.then(() => {}, () => {}));
|
|
175
|
+
return run;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function searchDocuments(query: string, options: { projectId?: string; limit?: number } = {}): SearchRow[] {
|
|
179
|
+
const text = String(query || '').trim();
|
|
180
|
+
if (!text) return [];
|
|
181
|
+
const projectId = String(options.projectId || '');
|
|
182
|
+
const limit = Math.max(1, Math.min(100, Number(options.limit) || 12));
|
|
183
|
+
const commonSelect = `SELECT source, entity_id AS entityId, project_id AS projectId, title,
|
|
184
|
+
snippet(search_fts, 4, '', '', ' … ', 56) AS snippet,
|
|
185
|
+
bm25(search_fts) AS rank FROM search_fts`;
|
|
186
|
+
try {
|
|
187
|
+
if (!ftsEnabled) throw new Error('fts5 unavailable');
|
|
188
|
+
if ([...text].length >= 3) {
|
|
189
|
+
const phrase = `"${text.replace(/"/g, '""')}"`;
|
|
190
|
+
return database.prepare(`${commonSelect}
|
|
191
|
+
WHERE search_fts MATCH ? AND (? = '' OR project_id = ?)
|
|
192
|
+
ORDER BY rank LIMIT ?`).all(phrase, projectId, projectId, limit) as SearchRow[];
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
// Invalid FTS syntax and short queries use the bounded LIKE fallback.
|
|
196
|
+
}
|
|
197
|
+
const like = `%${text.replace(/[\\%_]/g, '\\$&')}%`;
|
|
198
|
+
return database.prepare(`SELECT source, entity_id AS entityId, project_id AS projectId, title,
|
|
199
|
+
substr(content, 1, 2400) AS snippet, 0 AS rank FROM ${ftsEnabled ? 'search_fts' : 'search_documents'}
|
|
200
|
+
WHERE (title LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\') AND (? = '' OR project_id = ?)
|
|
201
|
+
LIMIT ?`).all(like, like, projectId, projectId, limit) as SearchRow[];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
importJsonFiles();
|
|
205
|
+
reindex('assets.json', read('assets.json', []));
|
|
206
|
+
reindex('memories.json', read('memories.json', []));
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
kind: 'sqlite',
|
|
210
|
+
read,
|
|
211
|
+
write,
|
|
212
|
+
remove,
|
|
213
|
+
resolve,
|
|
214
|
+
mutate,
|
|
215
|
+
searchDocuments,
|
|
216
|
+
close: () => database.close(),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = { createSqliteStore };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Incremental WHATWG-style SSE decoder for arbitrarily split byte chunks. */
|
|
2
|
+
class SSEDecoder {
|
|
3
|
+
private buffer = '';
|
|
4
|
+
|
|
5
|
+
push(chunk: string): string[] {
|
|
6
|
+
this.buffer += String(chunk || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
7
|
+
const events: string[] = [];
|
|
8
|
+
let boundary = this.buffer.indexOf('\n\n');
|
|
9
|
+
while (boundary >= 0) {
|
|
10
|
+
const block = this.buffer.slice(0, boundary);
|
|
11
|
+
this.buffer = this.buffer.slice(boundary + 2);
|
|
12
|
+
const data = this.dataFrom(block);
|
|
13
|
+
if (data !== null) events.push(`data: ${data}`);
|
|
14
|
+
boundary = this.buffer.indexOf('\n\n');
|
|
15
|
+
}
|
|
16
|
+
return events;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
flush(): string[] {
|
|
20
|
+
const block = this.buffer;
|
|
21
|
+
this.buffer = '';
|
|
22
|
+
const data = this.dataFrom(block);
|
|
23
|
+
return data === null ? [] : [`data: ${data}`];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private dataFrom(block: string): string | null {
|
|
27
|
+
if (!block.trim()) return null;
|
|
28
|
+
const values: string[] = [];
|
|
29
|
+
for (const line of block.split('\n')) {
|
|
30
|
+
if (!line || line.startsWith(':')) continue;
|
|
31
|
+
const separator = line.indexOf(':');
|
|
32
|
+
const field = separator < 0 ? line : line.slice(0, separator);
|
|
33
|
+
if (field !== 'data') continue;
|
|
34
|
+
let value = separator < 0 ? '' : line.slice(separator + 1);
|
|
35
|
+
if (value.startsWith(' ')) value = value.slice(1);
|
|
36
|
+
values.push(value);
|
|
37
|
+
}
|
|
38
|
+
return values.length ? values.join('\n') : null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { SSEDecoder };
|