cc-viewer 1.6.304 → 1.6.306
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/concepts/zh/ultraplan-origin.md +57 -0
- package/dist/assets/{App-DfsHLalW.css → App-LcZ7ezR-.css} +1 -1
- package/dist/assets/{App-CE9-RsMp.js → App-Ob0BsD2o.js} +1 -1
- package/dist/assets/{MdxEditorPanel-CDLhUUIj.js → MdxEditorPanel-oSs95ieb.js} +1 -1
- package/dist/assets/{Mobile-BkZFYw5X.js → Mobile-CVLG_J2s.js} +1 -1
- package/dist/assets/{index-BDK4eIE5.js → index-1bh2o4MD.js} +2 -2
- package/dist/assets/seqResourceLoaders-CC7nzyEk.js +2 -0
- package/dist/assets/{seqResourceLoaders-D-2EA12R.css → seqResourceLoaders-H8fAJXE5.css} +2 -2
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/interceptor.js +2 -2
- package/server/lib/approval-modal-prefs.js +1 -1
- package/server/lib/claude-md-discovery.js +1 -1
- package/server/lib/file-api.js +3 -1
- package/server/lib/im-lock.js +1 -1
- package/server/lib/log-stream.js +1 -1
- package/server/lib/perm-bridge.js +1 -1
- package/server/lib/skills-api.js +1 -1
- package/server/lib/turn-end-bridge.js +1 -1
- package/server/lib/voice-pack-manager.js +1 -1
- package/server/lib/workflow-live.js +162 -8
- package/server/lib/workflow-watcher.js +4 -1
- package/server/routes/ask-perm.js +2 -2
- package/server/routes/files-fs.js +1 -1
- package/server/routes/im.js +5 -2
- package/server/server.js +2 -2
- package/dist/assets/seqResourceLoaders-aRa4CORe.js +0 -2
package/dist/index.html
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// 整体显示大小已弃用 CSS zoom:Electron 改用 webFrame.setZoomFactor(首屏抢占见
|
|
22
22
|
// electron/tab-content-preload.js),纯浏览器交由用户用浏览器自带快捷键缩放,故此处不再设 zoom。
|
|
23
23
|
</script>
|
|
24
|
-
<script type="module" crossorigin src="/assets/index-
|
|
24
|
+
<script type="module" crossorigin src="/assets/index-1bh2o4MD.js"></script>
|
|
25
25
|
<link rel="modulepreload" crossorigin href="/assets/vendor-antd-Bur5ZxWE.js">
|
|
26
26
|
<link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-Si44UqBp.js">
|
|
27
27
|
<link rel="modulepreload" crossorigin href="/assets/vendor-mdxeditor-Cco3AQJS.js">
|
package/package.json
CHANGED
package/server/interceptor.js
CHANGED
|
@@ -64,7 +64,7 @@ let _defaultConfig = null; // { origin, authType, model }
|
|
|
64
64
|
|
|
65
65
|
function _getActiveProfileFilePath() {
|
|
66
66
|
// _projectName/_logDir 声明在 ~line 218;本函数只会在这些变量初始化后被调用
|
|
67
|
-
// (_loadProxyProfile
|
|
67
|
+
// (_loadProxyProfile 的初始调用与 watchFile 挂载都被挪到 _projectName/_logDir 初始化之后;watchFile 回调、HTTP handler 也都在之后)
|
|
68
68
|
if (!_projectName || !_logDir) return null;
|
|
69
69
|
return join(_logDir, 'active-profile.json');
|
|
70
70
|
}
|
|
@@ -159,7 +159,7 @@ function getActiveProfileId() {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
// _loadProxyProfile 的初始调用 + watchFile 挂载挪到 _projectName/_logDir 初始化之后
|
|
162
|
-
// (见 "初始化日志文件路径" 段后的
|
|
162
|
+
// (见 "初始化日志文件路径" 段后的 _loadProxyProfile() + watchFile(PROFILE_PATH, …) 挂载),避免 TDZ。
|
|
163
163
|
|
|
164
164
|
// 纯函数:把 headers 里任意大小写的 authorization / x-api-key 替换为 profile 的 apiKey;
|
|
165
165
|
// 两者都不存在时强制植入 x-api-key(第三方代理最常见的鉴权形式)。
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// fields (modalEnabled, soundEnabled, notifyOnlyWhenHidden) plus the voicePack subtree.
|
|
6
6
|
// voice-pack-manager.js stays focused on the file/audio backing store.
|
|
7
7
|
//
|
|
8
|
-
// Both server/
|
|
8
|
+
// Both server/routes/preferences.js (handles POST /api/preferences) and src/AppBase.jsx (hydrate +
|
|
9
9
|
// handleVoicePackChange) use this so the merge contract is single-sourced.
|
|
10
10
|
|
|
11
11
|
import { EVENT_KEYS } from './voice-pack-events.js';
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 策略:
|
|
7
7
|
* - 项目候选:从 cwd 实际路径(realpath 后)向上走,每层若 <dir>/CLAUDE.md 存在且为文件,收一条。
|
|
8
8
|
* 终止条件:dir === dirname(dir)(fs root) / dir === homedir() / <dir>/.git 存在 / depth 达 8(任一即停)。
|
|
9
|
-
*
|
|
9
|
+
* 三种终止条件(homedir / .git / fs root)都先收当前层再停——pushIfFile 在终止判定之前无条件执行。
|
|
10
10
|
* - 全局候选:~/.claude/CLAUDE.md(以参数 claudeConfigDir 为准,由调用方传入,便于沙箱测试)。
|
|
11
11
|
* - 排序:项目候选按"靠近 cwd"在前,全局总是最后一条。
|
|
12
12
|
* - 去重:基于 realpath 去重;同一物理文件被多入口指向只留一条(保留先入者)。
|
package/server/lib/file-api.js
CHANGED
|
@@ -29,7 +29,9 @@ class FileApiError extends Error {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* Resolve and validate a file path.
|
|
32
|
+
* Resolve and validate a file path. No production callers (readFileContent does its own inline
|
|
33
|
+
* path validation); kept as a standalone, path-traversal-defending helper covered by
|
|
34
|
+
* test/file-api-gap.test.js — don't delete as "dead code" without removing that test.
|
|
33
35
|
* @param {string} cwd - project working directory
|
|
34
36
|
* @param {string} reqPath - requested path (relative or absolute)
|
|
35
37
|
* @param {boolean} isEditorSession - whether this is an editor session
|
package/server/lib/im-lock.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// - 获取用 openSync(path,'wx') 原子哨兵(与 workspace-registry.js 同款),保证并发只有一个赢家。
|
|
6
6
|
// - 内容写入走 temp + renameSyncWithRetry(与 file-api.js / saveWorkspaces 一致),避免读到半写 JSON。
|
|
7
7
|
// - 读方对 JSON.parse 失败一律容忍(返回 null),绝不据此删锁。
|
|
8
|
-
// -
|
|
8
|
+
// - 活性判定四态:dead(无锁)/ booting(已建锁未写 port 且在启动窗内)/ ready(已写 port 且 HTTP 身份探测通过)/ hung(pid 存活但探测失败或超启动窗)。
|
|
9
9
|
// 长跑 bot 不会更新 mtime,故不沿用 workspace-registry 的 mtime 陈旧判据,改用 PID 存活 + HTTP 身份探测。
|
|
10
10
|
// - 释放按身份(仅当锁的 pid === 调用方 pid 才 unlink),避免误删后继进程的锁。
|
|
11
11
|
import { openSync, closeSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, existsSync } from 'node:fs';
|
package/server/lib/log-stream.js
CHANGED
|
@@ -127,7 +127,7 @@ function extractDedupKey(raw) {
|
|
|
127
127
|
const ts = extractTimestamp(raw);
|
|
128
128
|
const urlMatch = raw.match(/"url"\s*:\s*"([^"]+)"/);
|
|
129
129
|
if (ts && urlMatch) return `${ts}|${urlMatch[1]}`;
|
|
130
|
-
// fallback: 无法提取 key
|
|
130
|
+
// fallback: 无法提取 key 时返回 null;调用方改用位置键 __nokey_<index> 入表(非内容哈希)。
|
|
131
131
|
return null;
|
|
132
132
|
}
|
|
133
133
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* (allow/deny) in the web UI, then outputs hookSpecificOutput.
|
|
9
9
|
*
|
|
10
10
|
* Exit 0 = success (stdout contains hookSpecificOutput with permissionDecision)
|
|
11
|
-
* Exit 1 =
|
|
11
|
+
* Exit 1 = error (malformed/missing stdin or invalid env); makes Claude Code log a hook error. Graceful fallback (cc-viewer not running / no decision) instead exits 0 with { continue: true } so the terminal UI proceeds without an error log.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { readFileSync } from 'node:fs';
|
package/server/lib/skills-api.js
CHANGED
|
@@ -231,7 +231,7 @@ export function moveSkill({ source, name, enable, projectDir = process.cwd(), ho
|
|
|
231
231
|
renameSync(from, to);
|
|
232
232
|
} catch (e) {
|
|
233
233
|
if (e.code === 'EXDEV') {
|
|
234
|
-
// 跨设备兜底:cp -r + rm -rf
|
|
234
|
+
// 跨设备兜底:cp -r + rm -rf(与 routes/files-fs.js 的 EXDEV 分支同款模式)
|
|
235
235
|
cpSync(from, to, { recursive: true });
|
|
236
236
|
rmSync(from, { recursive: true, force: true });
|
|
237
237
|
} else {
|
|
@@ -51,7 +51,7 @@ if (!port) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
// Drain stdin best-effort. Claude Code passes a JSON payload with session_id /
|
|
54
|
-
// transcript_path;
|
|
54
|
+
// transcript_path; both session_id and transcript_path are forwarded. Capped to 64 KB to defang any
|
|
55
55
|
// malformed huge payload().
|
|
56
56
|
let stdinData = '';
|
|
57
57
|
try {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Layout:
|
|
4
4
|
// <LOG_DIR>/voice-packs/<id>.<ext> ← user-uploaded audio
|
|
5
|
-
// <repo>/public/voice-packs/default/ ← bundled default pack (
|
|
5
|
+
// <repo>/public/voice-packs/default/ ← bundled default pack (default-butler — "Butler · 皇上系列")
|
|
6
6
|
//
|
|
7
7
|
// Why a UUID-keyed flat dir (no nested user-supplied paths): the audio id ends up
|
|
8
8
|
// in URL path (/api/voice-pack/audio/:id), so we whitelist [a-f0-9-]{8,64} and
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
* ├─ agent-<id>.meta.json {"agentType":...}
|
|
9
9
|
* └─ journal.jsonl started / result 事件(带 agentId)→ running / done 判定
|
|
10
10
|
*
|
|
11
|
-
* 推导出的模型与 normalizeWorkflowJournal
|
|
12
|
-
*
|
|
11
|
+
* 推导出的模型与 normalizeWorkflowJournal 同形。phases 由 parsePhasesFromScript 从生成脚本
|
|
12
|
+
* (workflows/scripts/<name>-<runId>.js 顶部 meta.phases)文本解析填充——运行中即可显示阶段列;
|
|
13
|
+
* 但 agent 的 phaseIndex 运行中仍为 null(无权威 agent→phase 映射),故前端按「有 phases 但无
|
|
14
|
+
* agent 带 numeric phaseIndex」走扁平 agent 列表,完成后由权威的 <runId>.json 快照接管 phase 分组。
|
|
13
15
|
*
|
|
14
16
|
* token 口径:input + output + cache_creation(运行中单调增;与完成快照略有出入,完成时被
|
|
15
17
|
* 权威值替换)。工具数与快照一致(统计 tool_use 块)。
|
|
@@ -24,6 +26,10 @@ import { _RUN_ID_RE as RUN_ID_RE } from './workflow-journal.js';
|
|
|
24
26
|
const MAX_AGENT_BYTES = 64 * 1024 * 1024;
|
|
25
27
|
const LABEL_MAX = 80;
|
|
26
28
|
const PROMPT_PREVIEW_MAX = 600; // 头部菱形 hover 预览的 prompt 截断长度(与 journal promptPreview 量级一致)
|
|
29
|
+
const MAX_SCRIPT_BYTES = 2 * 1024 * 1024; // 脚本超过此大小只读头部 SCRIPT_HEAD_BYTES(meta 恒在文件最前)
|
|
30
|
+
const SCRIPT_HEAD_BYTES = 256 * 1024; // 超大脚本时读取的头部字节数,足够覆盖 meta 块
|
|
31
|
+
const MAX_PHASES = 50; // 解析出的 phases 项数上限(防御异常脚本)
|
|
32
|
+
const PHASES_CACHE_MAX = 256; // _phasesCache 条目上限(防长跑服务端无界增长)
|
|
27
33
|
|
|
28
34
|
function projectsDir() {
|
|
29
35
|
return process.env.CCV_PROJECTS_DIR || join(getClaudeConfigDir(), 'projects');
|
|
@@ -45,15 +51,161 @@ function isInsideProjectsDir(realPath) {
|
|
|
45
51
|
return realPath === root || realPath.startsWith(root + sep);
|
|
46
52
|
}
|
|
47
53
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
54
|
+
/**
|
|
55
|
+
* 从 workflows/scripts/<name>-<runId>.js 反推 workflowName + 脚本绝对路径(运行中即可拿到)。
|
|
56
|
+
* 一次 readdirSync 同时给出 name 与 scriptPath,供 phases 解析复用,避免重复扫目录。
|
|
57
|
+
* @returns {{ name: string, scriptPath: string|null }}
|
|
58
|
+
*/
|
|
59
|
+
function deriveWorkflowScript(sessionDir, runId) {
|
|
50
60
|
try {
|
|
51
61
|
const scriptsDir = join(sessionDir, 'workflows', 'scripts');
|
|
62
|
+
const suffix = `-${runId}.js`;
|
|
52
63
|
for (const f of readdirSync(scriptsDir)) {
|
|
53
|
-
if (f.endsWith(
|
|
64
|
+
if (f.endsWith(suffix)) {
|
|
65
|
+
return { name: f.slice(0, -suffix.length), scriptPath: join(scriptsDir, f) };
|
|
66
|
+
}
|
|
54
67
|
}
|
|
55
68
|
} catch {}
|
|
56
|
-
return '';
|
|
69
|
+
return { name: '', scriptPath: null };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 从一个对象/数组字面量起始的开括号位置,做「字符串感知」的括号配对扫描,
|
|
74
|
+
* 返回配平的闭括号下标(含);不配平返回 -1。
|
|
75
|
+
* 进入 '...' / "..." 串态时不计括号、并处理 `\` 转义,故 detail 文本里的
|
|
76
|
+
* `]` `}` `'` 都不会破坏配对。
|
|
77
|
+
*/
|
|
78
|
+
function _matchBalanced(text, openIdx) {
|
|
79
|
+
const open = text[openIdx];
|
|
80
|
+
const close = open === '[' ? ']' : '}';
|
|
81
|
+
let depth = 0, inStr = false, quote = '', esc = false;
|
|
82
|
+
for (let i = openIdx; i < text.length; i++) {
|
|
83
|
+
const ch = text[i];
|
|
84
|
+
if (inStr) {
|
|
85
|
+
if (esc) { esc = false; continue; }
|
|
86
|
+
if (ch === '\\') { esc = true; continue; }
|
|
87
|
+
if (ch === quote) inStr = false;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (ch === "'" || ch === '"' || ch === '`') { inStr = true; quote = ch; continue; }
|
|
91
|
+
if (ch === '[' || ch === '{') depth++;
|
|
92
|
+
else if (ch === ']' || ch === '}') {
|
|
93
|
+
depth--;
|
|
94
|
+
if (depth === 0) return i;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return -1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 反转义 JS 单/双引号字符串字面量的内容(仅常见序列;够用且安全)。 */
|
|
101
|
+
function _unescapeStr(s) {
|
|
102
|
+
return s.replace(/\\(['"`\\nrt])/g, (_, c) =>
|
|
103
|
+
c === 'n' ? '\n' : c === 'r' ? '\r' : c === 't' ? '\t' : c);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 在一段对象字面量文本里抽取 `key: '...'`(字符串感知,处理转义)。无则 null。
|
|
108
|
+
* key 前要求边界字符(`{` / `,` / 空白 / 串首),避免 subtitle/subdetail 等更长键名
|
|
109
|
+
* 子串命中 title/detail。
|
|
110
|
+
*/
|
|
111
|
+
function _extractStrField(objText, key) {
|
|
112
|
+
const re = new RegExp(`(?:^|[{,\\s])${key}\\s*:\\s*(['"\`])`);
|
|
113
|
+
const m = re.exec(objText);
|
|
114
|
+
if (!m) return null;
|
|
115
|
+
const quote = m[1];
|
|
116
|
+
let i = m.index + m[0].length, esc = false, out = '';
|
|
117
|
+
for (; i < objText.length; i++) {
|
|
118
|
+
const ch = objText[i];
|
|
119
|
+
if (esc) { out += ch; esc = false; continue; }
|
|
120
|
+
if (ch === '\\') { out += ch; esc = true; continue; }
|
|
121
|
+
if (ch === quote) return _unescapeStr(out);
|
|
122
|
+
out += ch;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 纯文本解析 workflow 脚本顶部 `export const meta = { ... phases: [{title, detail?}, ...] }`。
|
|
129
|
+
* 绝不 import/eval 脚本(执行不可信代码)。输出与 normalizeWorkflowJournal 同形:
|
|
130
|
+
* [{ index:(从1), title:string, detail:string('' if missing) }]
|
|
131
|
+
* 任何异常/不匹配/无 phases 一律返回 []。
|
|
132
|
+
*/
|
|
133
|
+
export function parsePhasesFromScript(scriptText) {
|
|
134
|
+
if (typeof scriptText !== 'string' || !scriptText) return [];
|
|
135
|
+
// 框定 meta 块(meta 恒为顶部纯字面量)
|
|
136
|
+
const metaKey = scriptText.match(/export\s+const\s+meta\s*=\s*\{/);
|
|
137
|
+
if (!metaKey) return [];
|
|
138
|
+
const metaOpen = scriptText.indexOf('{', metaKey.index);
|
|
139
|
+
const metaClose = _matchBalanced(scriptText, metaOpen);
|
|
140
|
+
if (metaClose === -1) return [];
|
|
141
|
+
const metaBody = scriptText.slice(metaOpen, metaClose + 1);
|
|
142
|
+
|
|
143
|
+
// 在 meta 内定位 phases 数组(字符串感知配对,detail 内的 `]` 不影响)
|
|
144
|
+
const phasesKey = metaBody.match(/phases\s*:\s*\[/);
|
|
145
|
+
if (!phasesKey) return [];
|
|
146
|
+
const arrOpen = metaBody.indexOf('[', phasesKey.index);
|
|
147
|
+
const arrClose = _matchBalanced(metaBody, arrOpen);
|
|
148
|
+
if (arrClose === -1) return [];
|
|
149
|
+
const arrBody = metaBody.slice(arrOpen + 1, arrClose);
|
|
150
|
+
|
|
151
|
+
// 逐项切出 {...} 对象(字符串感知配对),抽 title/detail
|
|
152
|
+
const phases = [];
|
|
153
|
+
let i = 0;
|
|
154
|
+
while (i < arrBody.length && phases.length < MAX_PHASES) {
|
|
155
|
+
const objOpen = arrBody.indexOf('{', i);
|
|
156
|
+
if (objOpen === -1) break;
|
|
157
|
+
const objClose = _matchBalanced(arrBody, objOpen);
|
|
158
|
+
if (objClose === -1) break;
|
|
159
|
+
const objText = arrBody.slice(objOpen, objClose + 1);
|
|
160
|
+
const title = _extractStrField(objText, 'title');
|
|
161
|
+
if (title !== null) {
|
|
162
|
+
phases.push({ index: phases.length + 1, title, detail: _extractStrField(objText, 'detail') || '' });
|
|
163
|
+
}
|
|
164
|
+
i = objClose + 1;
|
|
165
|
+
}
|
|
166
|
+
return phases;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// scriptPath → { mtimeMs, size, phases }。脚本运行中不可变,命中即跳过读盘+解析;
|
|
170
|
+
// edit-then-resume(mtime/size 变)则重解析。size 上限淘汰防长跑无界增长。
|
|
171
|
+
const _phasesCache = new Map();
|
|
172
|
+
|
|
173
|
+
/** 安全读取并(带缓存地)解析脚本 phases。失败/越界一律 []。 */
|
|
174
|
+
function readPhasesCached(scriptPath) {
|
|
175
|
+
if (!scriptPath) return [];
|
|
176
|
+
let real;
|
|
177
|
+
try { real = realpathSync(scriptPath); } catch { return []; }
|
|
178
|
+
if (!isInsideProjectsDir(real)) return [];
|
|
179
|
+
|
|
180
|
+
let mtimeMs, size;
|
|
181
|
+
try { ({ mtimeMs, size } = statSync(real)); } catch { return []; }
|
|
182
|
+
|
|
183
|
+
const hit = _phasesCache.get(real);
|
|
184
|
+
if (hit && hit.mtimeMs === mtimeMs && hit.size === size) return hit.phases;
|
|
185
|
+
|
|
186
|
+
let text = '';
|
|
187
|
+
try {
|
|
188
|
+
if (size <= MAX_SCRIPT_BYTES) {
|
|
189
|
+
text = readFileSync(real, 'utf-8');
|
|
190
|
+
} else {
|
|
191
|
+
const fd = openSync(real, 'r');
|
|
192
|
+
try {
|
|
193
|
+
const buf = Buffer.allocUnsafe(SCRIPT_HEAD_BYTES);
|
|
194
|
+
const n = readSync(fd, buf, 0, SCRIPT_HEAD_BYTES, 0);
|
|
195
|
+
text = buf.subarray(0, n).toString('utf-8');
|
|
196
|
+
} finally { closeSync(fd); }
|
|
197
|
+
}
|
|
198
|
+
} catch { return []; }
|
|
199
|
+
|
|
200
|
+
const phases = parsePhasesFromScript(text);
|
|
201
|
+
if (_phasesCache.size >= PHASES_CACHE_MAX) _phasesCache.clear();
|
|
202
|
+
_phasesCache.set(real, { mtimeMs, size, phases });
|
|
203
|
+
return phases;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** 测试辅助:清空 phases 解析缓存。 */
|
|
207
|
+
export function __clearPhasesCacheForTests() {
|
|
208
|
+
_phasesCache.clear();
|
|
57
209
|
}
|
|
58
210
|
|
|
59
211
|
// 每文件增量解析缓存:filePath → { mtimeMs, size, offset, partial(Buffer), acc }。
|
|
@@ -194,7 +346,9 @@ export function deriveLiveJournal(runDir, runId) {
|
|
|
194
346
|
const { done } = readResumeJournal(runDir);
|
|
195
347
|
// sessionDir = runDir 上溯三级(runId → workflows → subagents → sessionDir)
|
|
196
348
|
const sessionDir = dirname(dirname(dirname(runDir)));
|
|
197
|
-
const workflowName =
|
|
349
|
+
const { name: workflowName, scriptPath } = deriveWorkflowScript(sessionDir, runId);
|
|
350
|
+
// 运行中从生成脚本文本解析 meta.phases 填充阶段列(权威 <runId>.json 落盘后由完成快照接管)。
|
|
351
|
+
const phases = readPhasesCached(scriptPath);
|
|
198
352
|
|
|
199
353
|
const agents = [];
|
|
200
354
|
let totalTokens = 0, totalToolCalls = 0;
|
|
@@ -248,7 +402,7 @@ export function deriveLiveJournal(runDir, runId) {
|
|
|
248
402
|
totalToolCalls,
|
|
249
403
|
defaultModel: '',
|
|
250
404
|
startTime,
|
|
251
|
-
phases
|
|
405
|
+
phases,
|
|
252
406
|
agents,
|
|
253
407
|
live: true,
|
|
254
408
|
};
|
|
@@ -162,7 +162,10 @@ export function unwatchWorkflowDir(workflowsDir) {
|
|
|
162
162
|
function _liveSignature(data) {
|
|
163
163
|
if (!data) return 'none';
|
|
164
164
|
const states = data.agents.map(a => `${a.agentId}:${a.state}:${a.tokens}:${a.toolCalls}`).join('|');
|
|
165
|
-
|
|
165
|
+
// 纳入 phases 摘要:edit-then-resume 改写脚本 meta.phases(agent 计数/态未变)时也能触发重播,
|
|
166
|
+
// 否则 readPhasesCached 已按 mtime/size 失效重解析了新 phases,却因签名不变被这里抑制广播。
|
|
167
|
+
const phases = (data.phases || []).map(p => p.title).join(',');
|
|
168
|
+
return `${data.status}#${data.agentCount}#${data.totalTokens}#${data.totalToolCalls}#${phases}#${states}`;
|
|
166
169
|
}
|
|
167
170
|
|
|
168
171
|
// runDir = <sessionDir>/subagents/workflows/<runId> → 上溯三级到 sessionDir,查权威快照是否已落盘
|
|
@@ -123,8 +123,8 @@ function askHook(req, res, parsedUrl, isLocal, deps) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
// TOCTOU 防御:占位 id 之前先注册 res.on('close'),否则 await runWaterfallHook 期间 client
|
|
126
|
-
// abort 的 close 事件落空 → entry
|
|
127
|
-
// POST 的 do-while 都通过 collision check 后 set 互相覆盖(first res 永泄漏到
|
|
126
|
+
// abort 的 close 事件落空 → entry 残到 HOOK_TIMEOUT(24h)。占位 set 提前到 await 之前防两条同 ms 并发
|
|
127
|
+
// POST 的 do-while 都通过 collision check 后 set 互相覆盖(first res 永泄漏到 HOOK_TIMEOUT)。
|
|
128
128
|
const _placeholderEntry = { questions, res, timer: null, createdAt: Date.now(), shortPoll: shortPollMode };
|
|
129
129
|
deps.pendingAskHooks.set(id, _placeholderEntry);
|
|
130
130
|
deps.persistAskEntry(id, _placeholderEntry);
|
|
@@ -35,7 +35,7 @@ function upload(req, res, parsedUrl, isLocal, deps) {
|
|
|
35
35
|
if (totalSize > MAX_UPLOAD) {
|
|
36
36
|
aborted = true;
|
|
37
37
|
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
38
|
-
res.end(JSON.stringify({ error: 'File too large (max
|
|
38
|
+
res.end(JSON.stringify({ error: 'File too large (max 100MB)' }));
|
|
39
39
|
req.destroy();
|
|
40
40
|
return;
|
|
41
41
|
}
|
package/server/routes/im.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// GET /api/im/:platform/status — public; remote callers get only enabled+hasSecret+connection,
|
|
4
4
|
// the local (admin) caller additionally gets plaintext secrets + process info.
|
|
5
5
|
// POST /api/im/:platform/config — loopback-only; save creds, then drive the process manager
|
|
6
|
-
// (enable→stop+spawn worker, disable→stop).
|
|
7
|
-
//
|
|
6
|
+
// (enable→stop+spawn worker, disable→stop). Allowlist is optional:
|
|
7
|
+
// enabling with an empty one warns (not blocked) since the worker
|
|
8
|
+
// runs with --dangerously-skip-permissions.
|
|
8
9
|
// POST /api/im/:platform/test — loopback-only; validate creds (fetch an access token).
|
|
9
10
|
// POST /api/im/:platform/process — loopback-only; {action:start|stop|restart} the detached worker.
|
|
10
11
|
// GET /api/im/:platform/logs — resolve the worker's latest .jsonl (for the records popup).
|
|
@@ -115,6 +116,8 @@ function imConfigPost(req, res, parsedUrl, isLocal, deps) {
|
|
|
115
116
|
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
116
117
|
return;
|
|
117
118
|
}
|
|
119
|
+
// 【已评审确认 / ACCEPTED】允许空白名单是有意的产品取舍,非缺陷——code review 请勿反复 flag。
|
|
120
|
+
// 缓解层:PreToolUse permissions.deny(perm-bridge/im-deny,独立于白名单)始终硬拦截 + 服务端审计日志。
|
|
118
121
|
// 发送者白名单为非必填:启用时若白名单为空,不再硬拦截,而是允许保存(前端会弹安全警告)。
|
|
119
122
|
// 安全提示:worker 以 --dangerously-skip-permissions 运行;白名单为空时运行期退化为
|
|
120
123
|
// bind-first-conversation(im-bridge-core.js)——首个向机器人发消息的会话被绑定,该会话内任何人
|
package/server/server.js
CHANGED
|
@@ -1982,7 +1982,7 @@ export function broadcastTurnEnd(sessionId = null, ts = Date.now()) {
|
|
|
1982
1982
|
}
|
|
1983
1983
|
|
|
1984
1984
|
// 流式状态 SSE 推送定时器:检测 streamingState 变化并广播给所有客户端。
|
|
1985
|
-
// rising-edge → turn_end flush
|
|
1985
|
+
// rising-edge → turn_end cancel(丢弃 pending,不 flush)由 _observeStreamingTick 统一处理。
|
|
1986
1986
|
let _streamingStatusTimer = null;
|
|
1987
1987
|
// 启动后 30s 的更新检查 timer 句柄。必须可清理:
|
|
1988
1988
|
// - .unref() 防止它把事件循环 keep-alive 30s(测试进程靠 --test-force-exit 兜底是时序侥幸);
|
|
@@ -1995,7 +1995,7 @@ function startStreamingStatusTimer() {
|
|
|
1995
1995
|
if (isSdkMode) return;
|
|
1996
1996
|
const isActive = streamingState.active;
|
|
1997
1997
|
const wasActive = _lastCliActive;
|
|
1998
|
-
// 统一走 _observeStreamingTick:内部负责 rising-edge cancel
|
|
1998
|
+
// 统一走 _observeStreamingTick:内部负责 rising-edge cancel(丢弃 pending turn_end,不 flush)+ 更新 _lastCliActive。
|
|
1999
1999
|
_observeStreamingTick(isActive, 'cli');
|
|
2000
2000
|
const changed = wasActive !== isActive;
|
|
2001
2001
|
if (changed || isActive) {
|