@thincoder/core 0.9.5 → 0.10.1
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/CHANGELOG.md +36 -0
- package/README.md +1 -1
- package/abort-provenance.mjs +10 -10
- package/advisor/citations.mjs +5 -5
- package/advisor/compaction.mjs +16 -15
- package/advisor/loop.mjs +12 -12
- package/advisor/messages.mjs +6 -6
- package/advisor/project-context.mjs +5 -5
- package/advisor/repos.mjs +3 -3
- package/advisor/run.mjs +22 -12
- package/advisor.mjs +4 -4
- package/agent/assemble.mjs +111 -0
- package/agent/chat-call.mjs +68 -0
- package/agent/child-marks.mjs +2 -2
- package/agent/completion.mjs +9 -2
- package/agent/continue-decision.mjs +25 -0
- package/agent/dispatch-gates.mjs +140 -0
- package/agent/dispatch-run.mjs +167 -0
- package/agent/dispatch.mjs +54 -296
- package/agent/family-tools.mjs +5 -5
- package/agent/helpers.mjs +53 -3
- package/agent/live-beat.mjs +49 -0
- package/agent/post-turn.mjs +4 -8
- package/agent/record-results.mjs +23 -20
- package/agent/relay-prefix.mjs +2 -2
- package/agent/run-stages.mjs +15 -11
- package/agent/run-start.mjs +111 -0
- package/agent/setup-reminders.mjs +9 -9
- package/agent/setup.mjs +32 -4
- package/agent/spawn-child.mjs +9 -9
- package/agent/suspension.mjs +89 -29
- package/agent/timers.mjs +113 -0
- package/agent/turn-loop.mjs +244 -0
- package/agent/write-gate.mjs +103 -6
- package/agent-tools/advisor-async.mjs +9 -9
- package/agent-tools/advisor-settle.mjs +12 -12
- package/agent-tools/advisor.mjs +36 -41
- package/agent-tools/async-discard.mjs +44 -1
- package/agent-tools/async-settle.mjs +21 -18
- package/agent-tools/audit-block.mjs +7 -7
- package/agent-tools/bash-async.mjs +281 -0
- package/agent-tools/batch-lifecycle.mjs +57 -18
- package/agent-tools/batch-paths.mjs +52 -8
- package/agent-tools/batch-skeleton.mjs +19 -1
- package/agent-tools/batch.mjs +12 -65
- package/agent-tools/checkpoint.mjs +121 -0
- package/agent-tools/child-permission.mjs +5 -3
- package/agent-tools/consult.mjs +56 -56
- package/agent-tools/context.mjs +4 -20
- package/agent-tools/design-token.mjs +3 -3
- package/agent-tools/digest-budget.mjs +1 -1
- package/agent-tools/eng.mjs +5 -5
- package/agent-tools/escalate-async.mjs +16 -11
- package/agent-tools/goal.mjs +3 -10
- package/agent-tools/panel-blocks.mjs +2 -2
- package/agent-tools/parent-channel.mjs +3 -13
- package/agent-tools/plan.mjs +3 -4
- package/agent-tools/read-history.mjs +12 -28
- package/agent-tools/recent-changes.mjs +4 -5
- package/agent-tools/review-facts.mjs +1 -1
- package/agent-tools/settings.mjs +47 -20
- package/agent-tools/skill.mjs +3 -5
- package/agent-tools/spawn-gates.mjs +3 -3
- package/agent-tools/subagent-actions-query.mjs +270 -0
- package/agent-tools/subagent-actions.mjs +21 -265
- package/agent-tools/subagent-async.mjs +25 -20
- package/agent-tools/subagent-panel.mjs +33 -4
- package/agent-tools/subagent-run.mjs +36 -21
- package/agent-tools/subagent-scheduler.mjs +28 -27
- package/agent-tools/subagent-spawn.mjs +45 -14
- package/agent-tools/subagent.mjs +33 -53
- package/agent-tools/task.mjs +3 -9
- package/agent-tools/timer.mjs +12 -12
- package/agent-tools/verify.mjs +14 -13
- package/agent-tools.mjs +0 -1
- package/agent.mjs +18 -355
- package/attachments.mjs +125 -0
- package/auto-think.mjs +1 -1
- package/config-presets.mjs +11 -6
- package/config-watch.mjs +83 -0
- package/config.mjs +17 -4
- package/context-degrade.mjs +81 -0
- package/context-echo.mjs +88 -0
- package/context-push.mjs +51 -0
- package/context.mjs +8 -187
- package/conventions.mjs +110 -161
- package/declaration.mjs +194 -0
- package/embedding.mjs +3 -1
- package/env-ref.mjs +81 -0
- package/explore-distill.mjs +2 -3
- package/file-links.mjs +53 -0
- package/file-refs.mjs +103 -0
- package/generate-title.mjs +28 -4
- package/history-window.mjs +17 -2
- package/i18n.mjs +2 -0
- package/index-discover.mjs +7 -7
- package/ledger-cmd.mjs +34 -126
- package/ledger-db.mjs +10 -2
- package/ledger-tools.mjs +188 -0
- package/ledger.mjs +9 -4
- package/log.mjs +12 -7
- package/manifest-discovery.mjs +131 -0
- package/manifest-schema.mjs +166 -0
- package/manifest.mjs +18 -217
- package/mcp.mjs +4 -1
- package/memory/code-search.mjs +149 -0
- package/memory/code-sync.mjs +89 -230
- package/memory/core.mjs +47 -32
- package/memory/docs.mjs +63 -262
- package/memory/file-list.mjs +89 -0
- package/memory/file-walk.mjs +7 -4
- package/memory/memory-tool.mjs +229 -0
- package/memory/schema.mjs +18 -3
- package/memory/sweep.mjs +289 -0
- package/memory/sync-tail.mjs +81 -0
- package/memory-status.mjs +69 -0
- package/memory.mjs +3 -0
- package/model-specs.mjs +16 -7
- package/notify-policy.mjs +51 -0
- package/package.json +1 -1
- package/peer-claims.mjs +2 -2
- package/peer-domains.mjs +7 -11
- package/peer-instances.mjs +9 -10
- package/process-probe-exec.mjs +187 -0
- package/process-probe.mjs +27 -175
- package/prompt-overlays.mjs +11 -10
- package/prompts/advisor-design.md +2 -2
- package/prompts/advisor-round2.md +1 -1
- package/prompts/advisor-round3.md +1 -1
- package/prompts/common.md +16 -8
- package/prompts/discipline-engineering.md +57 -16
- package/prompts/discipline-normal.md +6 -3
- package/prompts/persona-coder.md +0 -1
- package/prompts/persona-eng-coder.md +1 -5
- package/prompts/persona-eng-designer.md +2 -3
- package/prompts/persona-engineering.md +32 -11
- package/prompts/persona-explore.md +0 -1
- package/prompts/persona-normal.md +1 -1
- package/prompts/persona-plan.md +3 -4
- package/prompts/subagent-base.md +14 -0
- package/provider/core.mjs +11 -48
- package/provider/list-models.mjs +3 -0
- package/provider/responses-request.mjs +237 -0
- package/provider/responses.mjs +6 -228
- package/provider-flows.mjs +248 -0
- package/queued.mjs +90 -0
- package/rules.mjs +209 -2
- package/session-guard.mjs +27 -7
- package/session-lifecycle.mjs +90 -18
- package/session-rename.mjs +5 -1
- package/session-slot-claims.mjs +230 -0
- package/session-slot-scan.mjs +299 -0
- package/session-slot-verify.mjs +304 -0
- package/session-slot-write.mjs +74 -8
- package/session-slots-manifest.mjs +163 -241
- package/session-slots.mjs +99 -71
- package/session.mjs +30 -10
- package/shell-candidates.mjs +78 -0
- package/text-budget.mjs +2 -2
- package/think-off.mjs +49 -0
- package/token-ttl.mjs +15 -4
- package/tool-args.mjs +73 -0
- package/tool-docs/advisor.md +9 -0
- package/tool-docs/batch.md +1 -0
- package/tool-docs/code_search.md +1 -0
- package/tool-docs/consult_start.md +5 -0
- package/tool-docs/consult_stop.md +3 -0
- package/tool-docs/context.md +7 -0
- package/tool-docs/doc_search.md +1 -0
- package/tool-docs/edit.md +2 -13
- package/tool-docs/eng.md +1 -0
- package/tool-docs/execute.md +1 -9
- package/tool-docs/git.md +25 -51
- package/tool-docs/goal.md +6 -0
- package/tool-docs/ledger_add.md +1 -0
- package/tool-docs/ledger_close.md +1 -0
- package/tool-docs/ledger_count.md +1 -0
- package/tool-docs/ledger_query.md +1 -0
- package/tool-docs/ledger_update.md +1 -0
- package/tool-docs/main_history.md +1 -0
- package/tool-docs/memory.md +10 -0
- package/tool-docs/notify_parent.md +9 -0
- package/tool-docs/peer_instances.md +1 -0
- package/tool-docs/plan.md +5 -0
- package/tool-docs/process.md +7 -3
- package/tool-docs/read_history.md +7 -0
- package/tool-docs/recent_changes.md +1 -0
- package/tool-docs/repo_outline.md +1 -0
- package/tool-docs/settings.md +6 -0
- package/tool-docs/skill.md +5 -0
- package/tool-docs/subagent.md +31 -0
- package/tool-docs/task.md +7 -0
- package/tool-docs/timer.md +6 -0
- package/tool-docs/verify.md +1 -0
- package/tool-docs/wait_for.md +2 -1
- package/tools/bash.mjs +28 -4
- package/tools/edit-batch.mjs +4 -4
- package/tools/edit-diff.mjs +4 -23
- package/tools/exec-run.mjs +102 -4
- package/tools/execute.mjs +5 -2
- package/tools/file.mjs +4 -9
- package/tools/git-run.mjs +1 -1
- package/tools/git.mjs +0 -24
- package/tools/index.mjs +2 -1
- package/tools/linter.mjs +2 -1
- package/tools/node-child.mjs +21 -0
- package/tools/ops.mjs +50 -13
- package/tools/patch.mjs +4 -2
- package/tools/repomap.mjs +20 -9
- package/tools/shared.mjs +8 -6
- package/tools/web.mjs +5 -2
- package/tools/write-path.mjs +1 -1
- package/traces/trace-store.mjs +17 -14
- package/vision-reader.mjs +76 -0
- package/agent-tools/batch-segment.mjs +0 -18
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-status.mjs — 记忆 / 索引状态**只读出口**(R2 · 桌面功能对位批 · 台账 #412 正解):三表计数一处出。
|
|
3
|
+
*
|
|
4
|
+
* 判据(父侧裁 —— 端侧不直读核内表;`docs/desktop/design/PROJECT.md` §10 N 行):
|
|
5
|
+
* - **只读**:本档零写面(无 DDL ∕ DML ∕ 迁移)+ 零动作面(无 sync ∕ reindex ∕ 建库);
|
|
6
|
+
* 句柄装配归调用面(`memory` 首参 = 已开句柄)。
|
|
7
|
+
* - **零端名**:键名 = 表名 ∕ 通用计数词,无端侧语汇 —— 各端同面同形(第二消费面即本出口本身)。
|
|
8
|
+
* - **origin 归一**:`origin` 给定 ⇒ 三表皆按**归一键**限定(核写缝一律归一 —— `memory/origin.mjs`;
|
|
9
|
+
* 读面不归一 ⇒ 读不到本项目行);缺 ∕ 非串 ∕ 空串 ⇒ 全库计数(回执 `origin: null`)。
|
|
10
|
+
*
|
|
11
|
+
* 回执:
|
|
12
|
+
* ```
|
|
13
|
+
* { origin, tables: { files, code: { files, chunks }, doc: { files, chunks } },
|
|
14
|
+
* totals: { files, chunks }, indexed, dbBytes, origins: [{ origin, code, doc }] }
|
|
15
|
+
* ```
|
|
16
|
+
* `tables.files` = 记忆层(`files` 表:project ∕ team 两层 markdown 记忆)行数;`code` ∕ `doc` 各 =
|
|
17
|
+
* 去重文件数(`COUNT(DISTINCT path)`)与分块数(`COUNT(*)`);`totals` = code + doc 合计(端侧既有
|
|
18
|
+
* 索引读数同口径);`indexed` = 合计文件数 > 0(「本项目已建索引」读数 —— 读数非承诺)。
|
|
19
|
+
* P1 新增读数(§6.14 面③):`dbBytes` = 库文件 `statSync` 字节数(非文件型 ∕ 不可读 ⇒ null);
|
|
20
|
+
* `origins` = 逐 origin 行数(code ∕ doc 两表并集,键 = 库内**原样** origin,按名排序)。
|
|
21
|
+
*/
|
|
22
|
+
import { statSync } from "node:fs"
|
|
23
|
+
import { normalizeOrigin } from "./memory/origin.mjs"
|
|
24
|
+
|
|
25
|
+
/** 单表计数:`key` 非 null ⇒ 追加 `WHERE origin = ?`(三表皆有 origin 列 —— v5 ∕ v8 起)。 */
|
|
26
|
+
function countOf(memory, base, key) {
|
|
27
|
+
const sql = key === null ? base : `${base} WHERE origin = ?`
|
|
28
|
+
return key === null ? memory.db.prepare(sql).get() : memory.db.prepare(sql).get(key)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 三表计数只读出口(形见档头;`memory` = `createMemory()` 回执句柄)。 */
|
|
32
|
+
export function memoryStatus(memory, { origin } = {}) {
|
|
33
|
+
const normalized = normalizeOrigin(origin)
|
|
34
|
+
const scope = typeof normalized === "string" && normalized.length > 0 ? normalized : null
|
|
35
|
+
|
|
36
|
+
const files = countOf(memory, "SELECT COUNT(*) AS n FROM files", scope)?.n ?? 0
|
|
37
|
+
const codeRow = countOf(memory, "SELECT COUNT(DISTINCT path) AS files, COUNT(*) AS chunks FROM code_chunks", scope) ?? {}
|
|
38
|
+
const docRow = countOf(memory, "SELECT COUNT(DISTINCT path) AS files, COUNT(*) AS chunks FROM doc_chunks", scope) ?? {}
|
|
39
|
+
const code = { files: codeRow.files ?? 0, chunks: codeRow.chunks ?? 0 }
|
|
40
|
+
const doc = { files: docRow.files ?? 0, chunks: docRow.chunks ?? 0 }
|
|
41
|
+
const totalFiles = code.files + doc.files
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
origin: scope,
|
|
45
|
+
tables: { files, code, doc },
|
|
46
|
+
totals: { files: totalFiles, chunks: code.chunks + doc.chunks },
|
|
47
|
+
indexed: totalFiles > 0,
|
|
48
|
+
dbBytes: dbBytesOf(memory),
|
|
49
|
+
origins: originRows(memory),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 库文件字节数(P1——`statSync` 实读;非文件型 ∕ 不可读 ⇒ null)。 */
|
|
54
|
+
function dbBytesOf(memory) {
|
|
55
|
+
try { return memory.dbPath ? statSync(memory.dbPath).size : null } catch { return null }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 逐 origin 行数(P1——code ∕ doc 两表并集;键 = 库内原样 origin,按名排序)。 */
|
|
59
|
+
function originRows(memory) {
|
|
60
|
+
const map = new Map()
|
|
61
|
+
for (const [table, key] of [["code_chunks", "code"], ["doc_chunks", "doc"]]) {
|
|
62
|
+
for (const r of memory.db.prepare(`SELECT origin, COUNT(*) AS n FROM ${table} GROUP BY origin`).all()) {
|
|
63
|
+
const cur = map.get(r.origin) ?? { origin: r.origin, code: 0, doc: 0 }
|
|
64
|
+
cur[key] = Number(r.n)
|
|
65
|
+
map.set(r.origin, cur)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return [...map.values()].sort((a, b) => (a.origin < b.origin ? -1 : a.origin > b.origin ? 1 : 0))
|
|
69
|
+
}
|
package/memory.mjs
CHANGED
|
@@ -19,3 +19,6 @@ export { gitSync, codeSync, markIndexedCommit, codeSearch, ensureCodeEmbeddings,
|
|
|
19
19
|
|
|
20
20
|
// doc sync + search + memoryTools
|
|
21
21
|
export { docSync, docSearch, ensureDocEmbeddings, docSearchTool, memoryTools } from "./memory/docs.mjs"
|
|
22
|
+
|
|
23
|
+
// status read-only export (three-table counts single point — R2)
|
|
24
|
+
export { memoryStatus } from "./memory-status.mjs"
|
package/model-specs.mjs
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* multimodal: whether multimodal (image/vision input supported)
|
|
20
20
|
* thinkApi: thinking API type: "type"=thinking.type field / "effort"=reasoning_effort field
|
|
21
21
|
* thinkEnabledValue: when thinkApi is "type", the value used to enable thinking (default "enabled"; MiniMax uses "adaptive")
|
|
22
|
+
* thinkAlwaysOn: server-enforced thinking (true = always on — no off path; single source = MODEL-SPECS.md §16.3)
|
|
22
23
|
* reasoningEcho: reasoning_content cross-turn echo strategy: "required"=must echo (error if missing) / "optional"=echo optional (default: don't echo)
|
|
23
24
|
* reasoningEffortEnum: valid reasoning_effort enum values (if undeclared, no validation — passed through as-is)
|
|
24
25
|
* tempRange: valid temperature range [min, max] (if undeclared, no clamping)
|
|
@@ -63,16 +64,21 @@ const MODEL_SPECS = [
|
|
|
63
64
|
// GLM-5.3: thinking always-on (no "disabled"); effort converges to low/high/max — NOT the
|
|
64
65
|
// 7-level glm-5.2 enum (verified vs docs.bigmodel.cn GLM-5.3 page, 2026-08).
|
|
65
66
|
// 视觉面 **未探**(不声明 multimodal——保守;补探登记见 MODEL-SPECS.md §13.4)。
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
// 该事实 = 机制位 `thinkAlwaysOn`(单源 = `docs/core/design/MODEL-SPECS.md` §16.3)——**官方口径**
|
|
68
|
+
// (智谱 GLM-5.3 文档:thinking always-on、无 `disabled`)。
|
|
69
|
+
["glm-5.3", { context: 1_000_000, maxOutput: 128_000, thinking: true, thinkAlwaysOn: true, thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 1], noUsageStream: true }],
|
|
70
|
+
// glm-5.3-flash:始终思考——该事实 = 机制位 `thinkAlwaysOn`(单源 = `docs/core/design/MODEL-SPECS.md` §16.3)
|
|
71
|
+
// ——**族沿用**(族据 = `glm-5.3` 行);其余字段见其下行注与规格面。
|
|
72
|
+
["glm-5.3-flash", { context: 1_000_000, maxOutput: 131_072, thinking: true, thinkAlwaysOn: true, multimodal: true, thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 1], noUsageStream: true }],
|
|
68
73
|
// glm-5.3-flashx(GLM-5.3-FlashX,2026-09 上线)——独立行(不再蹭 flash 前缀行):maxOutput 131_072 =
|
|
69
74
|
// **校验级**(400「max_tokens…限制数值范围[1,131072]」);effort 枚举 ["low","high","max"] = **校验级**
|
|
70
75
|
// (400 原文点名:low/high/max 受理,其余拒绝)。multimodal = **实测**(8×8 纯红 PNG → 答「红色」)。
|
|
71
76
|
// thinking = **实测**且**始终思考**(`thinking:{type:"disabled"}` → 400;裸请求默认开)——off 动作在
|
|
72
|
-
// UI 侧为 no-op
|
|
77
|
+
// UI 侧为 no-op(服务端无关闭路径)。该事实 = 机制位 `thinkAlwaysOn`(单源 = `docs/core/design/MODEL-SPECS.md`
|
|
78
|
+
// §16.3)——**实测**(`thinking:{type:"disabled"}` → 400;裸请求默认开)。其余 = **族沿用** flash 行(context 网络口径未探边;
|
|
73
79
|
// thinkApi / reasoningEcho / tempRange / noUsageStream = 前缀命中路径日常在用)。
|
|
74
80
|
// flash 行 maxOutput 131_072 = **校验级**(#20 批 §1.2 同源 · 2026-09-25 批 §14.2 #1 收正)——两行独立(改一行不动另一行)。
|
|
75
|
-
["glm-5.3-flashx", { context: 1_000_000, maxOutput: 131_072, thinking: true, multimodal: true, thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 1], noUsageStream: true }],
|
|
81
|
+
["glm-5.3-flashx", { context: 1_000_000, maxOutput: 131_072, thinking: true, thinkAlwaysOn: true, multimodal: true, thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 1], noUsageStream: true }],
|
|
76
82
|
["glm-5.2", { context: 1_000_000, maxOutput: 128_000, thinking: true, thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["max", "xhigh", "high", "medium", "low", "minimal", "none"], tempRange: [0, 1], noUsageStream: true }],
|
|
77
83
|
["glm-5", { context: 1_000_000, maxOutput: 128_000, thinking: true, thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["max", "xhigh", "high", "medium", "low", "minimal", "none"], tempRange: [0, 1], noUsageStream: true }],
|
|
78
84
|
["glm-4", { context: 128_000, maxOutput: 32_000, thinking: true, thinkApi: "type", reasoningEcho: "optional", tempRange: [0, 1], noUsageStream: true }],
|
|
@@ -250,9 +256,11 @@ const SORTED_SPECS = [...MODEL_SPECS].sort((a, b) => b[0].length - a[0].length)
|
|
|
250
256
|
|
|
251
257
|
/** Single table lookup shared by specForModel / specMatch — prefix match (case-insensitive)
|
|
252
258
|
* with vendor-namespace stripping. Returns null on a miss (DEFAULT_SPEC is the caller-side
|
|
253
|
-
* fallback and is deliberately NOT returned here — `matched` needs the miss itself).
|
|
259
|
+
* fallback and is deliberately NOT returned here — `matched` needs the miss itself).
|
|
260
|
+
* 非串 model(外端脏载 / 手工档)归一等价空串 ⇒ 未命中 ⇒ 调用侧 DEFAULT_SPEC(#638 查表全性:
|
|
261
|
+
* 下游无类型门——`.toLowerCase` 只认串,非串归一后本函数恒 total,同 providerSpec 文档句)。 */
|
|
254
262
|
function lookupSpec(model) {
|
|
255
|
-
const m = (model
|
|
263
|
+
const m = (typeof model === "string" ? model : "").toLowerCase()
|
|
256
264
|
for (const [prefix, spec] of SORTED_SPECS) {
|
|
257
265
|
if (m.startsWith(prefix.toLowerCase())) return spec
|
|
258
266
|
}
|
|
@@ -271,7 +279,8 @@ function lookupSpec(model) {
|
|
|
271
279
|
* instead of silently degrading to the 128K default (IK5VGJ). The dedupe set is shared by
|
|
272
280
|
* specForModel / specMatch (PROVIDER.md §16 M5). */
|
|
273
281
|
function warnUnknownModel(model) {
|
|
274
|
-
|
|
282
|
+
// 非串 model(脏载)归一空串 ⇒ 不警告(未登记 ≠ 拼错——不刷屏;#638 与 lookupSpec 同归一)
|
|
283
|
+
const m = (typeof model === "string" ? model : "").toLowerCase()
|
|
275
284
|
if (m && !warnedModels.has(m)) {
|
|
276
285
|
warnedModels.add(m)
|
|
277
286
|
console.warn(`[config] model "${model}" not found in MODEL_SPECS — using default spec (128K context, 32K output). Check the model ID or add an alias.`)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* notify-policy.mjs — 完成提示面**策略**(失焦门 + 单档 + 载荷成形 + 词键)——「桌面处理流 · VSC 对齐」批 R6 上提产物。
|
|
3
|
+
*
|
|
4
|
+
* 上提源 = 桌面现状超集 `thincoder-desktop/src/main/notify.mjs`(零宿主依赖 —— **纯搬,零语义改**);
|
|
5
|
+
* 对位锚(迁移前坐标)= VSC `thincoder-vscode/src/extension/notify.mjs:9-18`(失焦门同判据)+
|
|
6
|
+
* `panel-callbacks.mjs:236`(`!autoTurn` 门径)。平台落子(Electron `Notification` vs
|
|
7
|
+
* VSC `showInformationMessage`)= 宿主能力面(真端差登记 ①)⇒ **装配留端**(桌面 =
|
|
8
|
+
* `thincoder-desktop/src/main/main.mjs`;VSC = `notify.mjs` 宿主包装——已随 parity-b4 迁移落形);本档只承策略面。
|
|
9
|
+
*
|
|
10
|
+
* `notify`(平台落子)· `focused`(焦态判据)· `reveal`(点击面)· `localeOf`(语言取值缝——VSC
|
|
11
|
+
* wrapper 携 `vscode.env.language`,`normalizeLocale` 归一 `zh-CN → zh`;缺省 = `agent.config?.locale`,
|
|
12
|
+
* 桌面径零改)四件注入 ⇒ 平 node 直测。词键一键自此持本档(值 = 原 VSC 逐字 `notify.done`——该键已随
|
|
13
|
+
* parity-b4 迁移自 `thincoder-vscode/locales/{zh,en}.json:222` 删)。触发单档(`!autoTurn` 判据在调用面 —— 单回合
|
|
14
|
+
* 执行面;VSC 同判据):
|
|
15
|
+
* 用户回合完成(VSC 逐字同判据 = `panel-callbacks.mjs:236` 的 `!autoTurn` 门)
|
|
16
|
+
* `title` = 会话标题(`agent.title`——槽装载面在场;空 ∕ 不可得 ⇒ **零携**);点击 = 聚焦窗口 ∕
|
|
17
|
+
* 打开 thincoder 视图(reveal 语义 —— 宿主面;不切会话)。
|
|
18
|
+
* 消费面 = 桌面 `thincoder-desktop/src/main/notify.mjs`(端胶水 re-export);VSC `notify.mjs`(宿主包装——已随 parity-b4 迁移落形)。
|
|
19
|
+
*/
|
|
20
|
+
import { normalizeLocale } from "./i18n.mjs"
|
|
21
|
+
|
|
22
|
+
/** 词键值(zh ∕ en 常量对:`notify.done` = 原 VSC `locales/{zh,en}.json:222` 逐字——该键已随 parity-b4 迁移自 locales 删)。 */
|
|
23
|
+
export const NOTIFY_TEXTS = Object.freeze({
|
|
24
|
+
zh: "ThinCoder:本轮完成",
|
|
25
|
+
en: "ThinCoder: turn complete",
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
/** 零动作面(`notify` 注入缺位 = 提示面缺位 ⇒ 零连带 —— 回合链不受累、不抛;沿「禁假造」口径不落假通知)。 */
|
|
29
|
+
const SILENT = Object.freeze({ turnDone: () => false })
|
|
30
|
+
|
|
31
|
+
/** 提示面工厂:`notify(payload)` = 平台落子(载荷 `{ title?, body, reveal? }`)· `focused()` = 窗口聚焦判据
|
|
32
|
+
* (真 ⇒ 门闭合 · 缺省 = 恒聚焦 ⇒ 零通知)· `reveal()` = 点击面(宿主语义)· `localeOf(agent)` =
|
|
33
|
+
* 语言取值缝(缺省 = `agent?.config?.locale`)。返回单档 `turnDone`(返回值 = 是否落子,测试读数面)。 */
|
|
34
|
+
export function createNotifier({ notify = null, focused = null, reveal = null, localeOf = null } = {}) {
|
|
35
|
+
if (typeof notify !== "function") return SILENT
|
|
36
|
+
const isFocused = typeof focused === "function" ? focused : () => true
|
|
37
|
+
const langOf = (agent) => (normalizeLocale(typeof localeOf === "function" ? localeOf(agent) : agent?.config?.locale) === "zh" ? "zh" : "en")
|
|
38
|
+
const fire = ({ agent = null } = {}) => {
|
|
39
|
+
if (isFocused()) return false // 失焦门:聚焦 = no-op(never noise —— VSC `thincoder-vscode/src/extension/notify.mjs:9-18` 同判据)
|
|
40
|
+
const payload = { body: NOTIFY_TEXTS[langOf(agent)] }
|
|
41
|
+
const title = typeof agent?.title === "string" && agent.title.trim() !== "" ? agent.title : null
|
|
42
|
+
if (title !== null) payload.title = title // 会话标题在场才携(不可得 ⇒ 零携)
|
|
43
|
+
if (typeof reveal === "function") payload.reveal = reveal
|
|
44
|
+
try { notify(payload) } catch (error) { console.error("[notify] dispatch failed:", error) } // 平台抛 ⇒ 零连带
|
|
45
|
+
return true
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
/** 用户回合完成(`!autoTurn` 判据在调用面 —— 单回合执行面成功径)。 */
|
|
49
|
+
turnDone(vars) { return fire(vars) },
|
|
50
|
+
}
|
|
51
|
+
}
|
package/package.json
CHANGED
package/peer-claims.mjs
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import { existsSync, readFileSync } from "node:fs"
|
|
24
24
|
import { join } from "node:path"
|
|
25
25
|
import { configDir } from "./config.mjs"
|
|
26
|
-
import { normalizeCwd, getSessionId,
|
|
26
|
+
import { normalizeCwd, getSessionId, sessionEnd, manifestPath, writeSessionFile } from "./session-slots.mjs"
|
|
27
27
|
|
|
28
28
|
/** 认领租约(§4.4.2):30 min——同目标再写 / 目标被已有认领覆盖 ⇒ `expiresAt = now + 本值`。 */
|
|
29
29
|
export const CLAIM_TTL_MS = 30 * 60 * 1000
|
|
@@ -175,7 +175,7 @@ export function flushPeerClaims(agent, now = claimsNow()) {
|
|
|
175
175
|
const payload = { ...base }
|
|
176
176
|
if (payload.sessionId === undefined) payload.sessionId = sessionId
|
|
177
177
|
if (payload.pid === undefined) payload.pid = process.pid
|
|
178
|
-
if (payload.end === undefined) payload.end =
|
|
178
|
+
if (payload.end === undefined) payload.end = sessionEnd()
|
|
179
179
|
if (payload.cwd === undefined) payload.cwd = normalizeCwd(cwd)
|
|
180
180
|
payload.claims = claims
|
|
181
181
|
payload.claimsUpdatedAt = now
|
package/peer-domains.mjs
CHANGED
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
|
|
22
22
|
import { statSync, readdirSync, readFileSync, unlinkSync } from "node:fs"
|
|
23
23
|
import { join, resolve } from "node:path"
|
|
24
|
-
import { normalizeCwd, getSessionId,
|
|
24
|
+
import { normalizeCwd, getSessionId, sessionEnd, writeSessionFile } from "./session-slots.mjs"
|
|
25
25
|
import { batchAlive } from "./peer-instances.mjs"
|
|
26
|
-
import { FILE_MUTATORS } from "./agent/helpers.mjs"
|
|
26
|
+
import { FILE_MUTATORS, toolTouchPaths } from "./agent/helpers.mjs"
|
|
27
27
|
// 认领面(§4.4)归口 `peer-claims.mjs`;re-export 路径与谓词保既有 import 面(单源不复制)。
|
|
28
28
|
import {
|
|
29
29
|
peersDir, peerFilePath, pathsOverlap, claimHits, claimNoteText, claimAge, claimLeft,
|
|
@@ -73,15 +73,11 @@ function statDirMtimeMs(dir) {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
76
|
+
/** 目标路径解析(#327 单源谓词 `toolTouchPaths`——`docs/core/design/TOOLS.md` §6.17:钩子裁决 /
|
|
77
|
+
* `file_ops` 无钩子 ⇒ 动作感知(`copy` ⇒ 只 `dest`;`move` / `rename` / 缺 `action` ⇒ source+dest);
|
|
78
|
+
* 其余单参兜底)。相对路径按 cwd 解析为绝对(resolve:绝对入参原样保留);畸形入参跳过。 */
|
|
79
79
|
export function peerWriteTargets(tool, args, cwd) {
|
|
80
|
-
const raw = tool
|
|
81
|
-
? tool.touchedPaths(args ?? {})
|
|
82
|
-
: tool?.name === "file_ops"
|
|
83
|
-
? [args?.source, args?.dest]
|
|
84
|
-
: [args?.path]
|
|
80
|
+
const raw = toolTouchPaths(tool, args) // 单源谓词(恒数组 · 恒零抛——原裸抛面收口)
|
|
85
81
|
const out = []
|
|
86
82
|
for (const p of raw) {
|
|
87
83
|
if (typeof p !== "string" || p.length === 0) continue
|
|
@@ -286,7 +282,7 @@ export function flushPeerDomains(agent) {
|
|
|
286
282
|
...base,
|
|
287
283
|
sessionId,
|
|
288
284
|
pid: process.pid,
|
|
289
|
-
end:
|
|
285
|
+
end: sessionEnd(),
|
|
290
286
|
cwd: normalizeCwd(agent.cwd),
|
|
291
287
|
domains: [...set],
|
|
292
288
|
updatedAt: Date.now(),
|
package/peer-instances.mjs
CHANGED
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { statSync } from "node:fs"
|
|
29
|
-
import { manifestPath, loadManifest, getSessionId,
|
|
29
|
+
import { manifestPath, loadManifest, getSessionId, sessionEnd } from "./session-slots.mjs"
|
|
30
30
|
import { batchAlive, batchAliveAsync, probeCmdlinesAsync, classifyEnd, isProductProc } from "./process-probe.mjs"
|
|
31
|
+
import { DESC } from "./tools/shared.mjs" // #15 描述外置:文本单点 = tool-docs/peer_instances.md
|
|
31
32
|
|
|
32
33
|
export { batchAlive } // 既有 import 面(peer-domains.mjs)零破——实现已外提 process-probe.mjs
|
|
33
34
|
|
|
@@ -78,10 +79,11 @@ function cachePut(cwd, entry) {
|
|
|
78
79
|
peerInstanceCache.set(cwd, entry)
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
/** manifest slotSessions → 按 sessionId 去重分组 [{ sessionId, pid, slots }]
|
|
82
|
+
/** manifest slotSessions → 按 sessionId 去重分组 [{ sessionId, pid, slots }](纯函数·零 IO)。
|
|
82
83
|
* sessionId 形如 "{pid}-{ts}-{rand}"(进程级——可去重分组,MULTI-INSTANCE-COLLAB §3.1);pid 不可解析
|
|
83
|
-
* 的条目跳过(存量清理归 saveManifest 的 cleanDeadOwners——本模块纯只读不写)。
|
|
84
|
-
|
|
84
|
+
* 的条目跳过(存量清理归 saveManifest 的 cleanDeadOwners——本模块纯只读不写)。
|
|
85
|
+
* **具名导出**(§3.1「聚合半段单源」):VSC 端引本件分组半段(端侧零本地副本)。 */
|
|
86
|
+
export function groupSlotSessions(m) {
|
|
85
87
|
const byId = new Map()
|
|
86
88
|
for (const [slot, sessionId] of Object.entries(m.slotSessions ?? {})) {
|
|
87
89
|
if (typeof sessionId !== "string" || sessionId.length === 0) continue
|
|
@@ -145,7 +147,7 @@ export async function peerInstances(cwd) {
|
|
|
145
147
|
const peers = []
|
|
146
148
|
const selfGroup = groups.find((g) => g.sessionId === myId)
|
|
147
149
|
if (selfGroup) {
|
|
148
|
-
peers.push({ pid: selfGroup.pid ?? process.pid, sessionId: myId, slots: selfGroup.slots, end:
|
|
150
|
+
peers.push({ pid: selfGroup.pid ?? process.pid, sessionId: myId, slots: selfGroup.slots, end: sessionEnd(), self: true })
|
|
149
151
|
}
|
|
150
152
|
for (const g of peerGroups) {
|
|
151
153
|
const end = known ? classifyEnd(cmdlines.get(g.pid)) : undefined
|
|
@@ -159,14 +161,11 @@ export async function peerInstances(cwd) {
|
|
|
159
161
|
/**
|
|
160
162
|
* L2 只读查询工具(MULTI-INSTANCE-COLLAB §3.3)——挂本模块导出。schema
|
|
161
163
|
* description 逐字锚(评审修正 #7——2026-09-06 定稿,双端照抄——禁止自行解释):
|
|
162
|
-
*
|
|
163
|
-
* workspace cwd. Returns [{ pid, end, sessionId, slots }]; never includes self;
|
|
164
|
-
* pure read — writes nothing."
|
|
164
|
+
* 文本本体 = `tool-docs/peer_instances.md`(DESC() 加载;#15 外置——文本逐字零改)。
|
|
165
165
|
*/
|
|
166
166
|
export const peerInstancesTool = {
|
|
167
167
|
name: "peer_instances",
|
|
168
|
-
description:
|
|
169
|
-
"peer_instances — read-only: list other live ThinCoder instances sharing this workspace cwd. Returns [{ pid, end, sessionId, slots }]; never includes self; pure read — writes nothing.",
|
|
168
|
+
description: DESC("peer_instances"), // #15 外置:文本单点 = tool-docs/peer_instances.md(逐字锚)
|
|
170
169
|
parameters: { type: "object", properties: {} },
|
|
171
170
|
readonly: true,
|
|
172
171
|
async execute(args, ctx) {
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* process-probe-exec.mjs — 探测执行面(#590① 自 `process-probe.mjs` 外提——CORE-UNIFICATION
|
|
3
|
+
* 行 10 预案兑现;语义逐字零变)。
|
|
4
|
+
*
|
|
5
|
+
* 归本档:双超时常量(`ALIVE_EXEC_TIMEOUT_MS` / `CMDLINE_EXEC_TIMEOUT_MS`)· 模块级测试注入缝
|
|
6
|
+
* (`_testImpl` + 两 setter——唯一读取点 = 本档四批量函数,同档免回引 / 免环)· `uniqPids` ·
|
|
7
|
+
* 输出解析族 · `execFileP` · 四批量探测函数(`batchAlive` / `batchAliveAsync` / `probeCmdlines` /
|
|
8
|
+
* `probeCmdlinesAsync`)。
|
|
9
|
+
* 留 `process-probe.mjs`:身份判据 · 三态判据 · 束 API · 单 pid 兼容面 · 清理面适配——该档对
|
|
10
|
+
* 四批量名与缝名 **import + re-export**(导入面逐字零改;`isProcessAlive` 经 import 取用本档
|
|
11
|
+
* `parseTasklistPids`,并自持 `execFileSync` import——win32 分支直调,不设转口)。
|
|
12
|
+
*
|
|
13
|
+
* 成本纪律与两形态语义(同步 / 异步共用同一解析族与同一注入缝)= `process-probe.mjs` 头注
|
|
14
|
+
* (单源——本档不重述)。
|
|
15
|
+
*/
|
|
16
|
+
import { execFile, execFileSync } from "node:child_process"
|
|
17
|
+
|
|
18
|
+
const ALIVE_EXEC_TIMEOUT_MS = 10_000
|
|
19
|
+
const CMDLINE_EXEC_TIMEOUT_MS = 15_000
|
|
20
|
+
|
|
21
|
+
// 模块级测试注入缝(default null = 生产实现;测试注入 + finally 恢复——见测试文件)
|
|
22
|
+
let _testImpl = null
|
|
23
|
+
|
|
24
|
+
/** 注入测试实现。aliveFn(pids) → Set<pid>|null;cmdlineFn(pids) → Map<pid,cmdline>|null
|
|
25
|
+
* (两缝均**批量**语义——一次调用拿全量)。返回前值便于测试保存恢复;**未传的槽置 null**(传 `{}` = 两槽同清,等价 reset)。 */
|
|
26
|
+
export function _setProcessProbeTestImpl({ aliveFn = undefined, cmdlineFn = undefined } = {}) {
|
|
27
|
+
const prev = _testImpl
|
|
28
|
+
_testImpl = { aliveFn: aliveFn ?? null, cmdlineFn: cmdlineFn ?? null }
|
|
29
|
+
return prev
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function _resetProcessProbeTestImpl() {
|
|
33
|
+
_testImpl = null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** pid 清单归一:去重 + 正整数(两探测共用——空清单调用面零 exec)。 */
|
|
37
|
+
export function uniqPids(pids) {
|
|
38
|
+
return [...new Set(pids.map(Number).filter((n) => Number.isInteger(n) && n > 0))]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── 输出解析族(同步 / 异步两形态**共用**——单源,防两形态漂移) ─────────────────
|
|
42
|
+
/** Windows tasklist CSV(`"name","pid"…`)→ 全量 PID 集合。 */
|
|
43
|
+
export function parseTasklistPids(out) {
|
|
44
|
+
const alive = new Set()
|
|
45
|
+
for (const line of out.split(/\r?\n/)) {
|
|
46
|
+
const m = line.match(/^"([^"]*)","(\d+)"/)
|
|
47
|
+
if (m) alive.add(Number(m[2]))
|
|
48
|
+
}
|
|
49
|
+
return alive
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Unix `ps -eo pid=` → 全量 PID 集合。 */
|
|
53
|
+
function parsePsPids(out) {
|
|
54
|
+
return new Set(out.split(/\r?\n/).map((l) => Number(l.trim())).filter((n) => Number.isInteger(n)))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Windows `Get-CimInstance … ConvertTo-Json` → `Map<pid, cmdline>`。 */
|
|
58
|
+
function parseCimCmdlines(out) {
|
|
59
|
+
const rows = JSON.parse(out.trim())
|
|
60
|
+
const map = new Map()
|
|
61
|
+
for (const r of Array.isArray(rows) ? rows : [rows]) {
|
|
62
|
+
if (r && Number.isInteger(r.ProcessId) && typeof r.CommandLine === "string") {
|
|
63
|
+
map.set(Number(r.ProcessId), r.CommandLine)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return map
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Unix `ps -eo pid=,args=` → `Map<pid, cmdline>`。 */
|
|
70
|
+
function parsePsArgs(out) {
|
|
71
|
+
const map = new Map()
|
|
72
|
+
for (const line of out.split(/\r?\n/)) {
|
|
73
|
+
const m = line.match(/^\s*(\d+)\s+(.*)$/)
|
|
74
|
+
if (m) map.set(Number(m[1]), m[2])
|
|
75
|
+
}
|
|
76
|
+
return map
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** execFile → Promise<string>(stdout;exec / 超时失败 ⇒ reject——调用面按「探测失败」降级)。 */
|
|
80
|
+
function execFileP(cmd, args, timeout) {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
execFile(cmd, args, { encoding: "utf8", timeout, windowsHide: true }, (err, stdout) => {
|
|
83
|
+
if (err) reject(err); else resolve(stdout)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 批量判活:单次 tasklist(Windows 全量 CSV)/ ps(Unix)拿全量 PID 集合 → 一次 exec
|
|
90
|
+
* 比对(修复 isProcessAlive 每 pid 一次 execSync 的成本——每回合 N 次 = 贵,探索 §4)。
|
|
91
|
+
* 返回存活 pid 的 Set;exec/解析失败 → null(调用方区分"探测失败"与"全死"——只读面按
|
|
92
|
+
* 无活伴降级;域面(peer-domains 死清理)在 null 时不得执行删除——探测失败 ≠ 死)。
|
|
93
|
+
* **纯存在性**:身份判据不在此层(D-MI11——过滤落点在读面 `peerInstances()` 与清理面
|
|
94
|
+
* `filterDeadOwners`;`peer-domains.mjs` 的域残留清理语义保持零变)。
|
|
95
|
+
*/
|
|
96
|
+
export function batchAlive(pids, { timeoutMs = ALIVE_EXEC_TIMEOUT_MS } = {}) {
|
|
97
|
+
const uniq = uniqPids(pids)
|
|
98
|
+
if (uniq.length === 0) return new Set()
|
|
99
|
+
if (_testImpl?.aliveFn) return _testImpl.aliveFn(uniq)
|
|
100
|
+
try {
|
|
101
|
+
if (process.platform === "win32") {
|
|
102
|
+
const output = execFileSync("tasklist", ["/FO", "CSV", "/NH"], {
|
|
103
|
+
encoding: "utf8", timeout: timeoutMs, stdio: ["ignore", "pipe", "ignore"],
|
|
104
|
+
})
|
|
105
|
+
const alive = parseTasklistPids(output)
|
|
106
|
+
return new Set(uniq.filter((pid) => alive.has(pid)))
|
|
107
|
+
}
|
|
108
|
+
const output = execFileSync("ps", ["-eo", "pid="], {
|
|
109
|
+
encoding: "utf8", timeout: timeoutMs, stdio: ["ignore", "pipe", "ignore"],
|
|
110
|
+
})
|
|
111
|
+
const alive = parsePsPids(output)
|
|
112
|
+
return new Set(uniq.filter((pid) => alive.has(pid)))
|
|
113
|
+
} catch {
|
|
114
|
+
return null // 探测失败(区别于全死)——调用方不得据此执行删除/判死副作用
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 异步对偶(TUI 假死批 · MULTI-INSTANCE-COLLAB §3.1):与 `batchAlive` 同语义(返回集 / null
|
|
120
|
+
* 降级一致),但 `execFile` + Promise 化——**读面**(每回合的 `peerInstances`)不得用同步 exec
|
|
121
|
+
* 占住事件循环。**同一注入缝** `_setProcessProbeTestImpl`(注入值可能是同步函数——经 await
|
|
122
|
+
* 消费);**清理面继续用同步版**(会话起点 / 认领路径,非每回合面——D-MI14 登记)。
|
|
123
|
+
*/
|
|
124
|
+
export async function batchAliveAsync(pids) {
|
|
125
|
+
const uniq = uniqPids(pids)
|
|
126
|
+
if (uniq.length === 0) return new Set()
|
|
127
|
+
if (_testImpl?.aliveFn) return _testImpl.aliveFn(uniq)
|
|
128
|
+
try {
|
|
129
|
+
if (process.platform === "win32") {
|
|
130
|
+
const alive = parseTasklistPids(await execFileP("tasklist", ["/FO", "CSV", "/NH"], ALIVE_EXEC_TIMEOUT_MS))
|
|
131
|
+
return new Set(uniq.filter((pid) => alive.has(pid)))
|
|
132
|
+
}
|
|
133
|
+
const alive = parsePsPids(await execFileP("ps", ["-eo", "pid="], ALIVE_EXEC_TIMEOUT_MS))
|
|
134
|
+
return new Set(uniq.filter((pid) => alive.has(pid)))
|
|
135
|
+
} catch {
|
|
136
|
+
return null // 探测失败(区别于全死)——只读面按「无活伴」降级
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 批量 cmdline 探测(决策③ A——一次 exec 拿全部 pid+cmdline):返回 Map<pid, cmdline>
|
|
142
|
+
* 或 null(exec 失败/解析失败)。Windows = 一次 Get-CimInstance(PowerShell);Unix =
|
|
143
|
+
* 一次 ps。仅在 manifest mtime 变化后跑一次(~百 ms 级——设计已接受);清理面同一批量
|
|
144
|
+
* 结果逐条消费(D-MI3 / N-MI3——不做每 pid 一次 exec)。
|
|
145
|
+
*/
|
|
146
|
+
export function probeCmdlines(pids, { timeoutMs = CMDLINE_EXEC_TIMEOUT_MS } = {}) {
|
|
147
|
+
const uniq = uniqPids(pids)
|
|
148
|
+
if (uniq.length === 0) return new Map()
|
|
149
|
+
if (_testImpl?.cmdlineFn) return _testImpl.cmdlineFn(uniq)
|
|
150
|
+
try {
|
|
151
|
+
let out
|
|
152
|
+
if (process.platform === "win32") {
|
|
153
|
+
// 一次 Get-CimInstance 拿全表 → node 侧过滤目标 pid(避免 shell 引号注入面)
|
|
154
|
+
out = execFileSync("powershell.exe",
|
|
155
|
+
["-NoProfile", "-NonInteractive", "-Command",
|
|
156
|
+
"Get-CimInstance Win32_Process | Select-Object ProcessId,CommandLine | ConvertTo-Json -Compress"],
|
|
157
|
+
{ encoding: "utf8", timeout: timeoutMs, stdio: ["ignore", "pipe", "ignore"] })
|
|
158
|
+
return parseCimCmdlines(out)
|
|
159
|
+
}
|
|
160
|
+
out = execFileSync("ps", ["-eo", "pid=,args="], {
|
|
161
|
+
encoding: "utf8", timeout: timeoutMs, stdio: ["ignore", "pipe", "ignore"],
|
|
162
|
+
})
|
|
163
|
+
return parsePsArgs(out)
|
|
164
|
+
} catch {
|
|
165
|
+
return null // 探测失败 → 端字段/身份缺省(调用方降级:保守保留)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 异步对偶(同 `probeCmdlines` 语义——`execFile` + Promise;同一注入缝;读面在用)。 */
|
|
170
|
+
export async function probeCmdlinesAsync(pids) {
|
|
171
|
+
const uniq = uniqPids(pids)
|
|
172
|
+
if (uniq.length === 0) return new Map()
|
|
173
|
+
if (_testImpl?.cmdlineFn) return _testImpl.cmdlineFn(uniq)
|
|
174
|
+
try {
|
|
175
|
+
if (process.platform === "win32") {
|
|
176
|
+
const out = await execFileP("powershell.exe",
|
|
177
|
+
["-NoProfile", "-NonInteractive", "-Command",
|
|
178
|
+
"Get-CimInstance Win32_Process | Select-Object ProcessId,CommandLine | ConvertTo-Json -Compress"],
|
|
179
|
+
CMDLINE_EXEC_TIMEOUT_MS)
|
|
180
|
+
return parseCimCmdlines(out)
|
|
181
|
+
}
|
|
182
|
+
const out = await execFileP("ps", ["-eo", "pid=,args="], CMDLINE_EXEC_TIMEOUT_MS)
|
|
183
|
+
return parsePsArgs(out)
|
|
184
|
+
} catch {
|
|
185
|
+
return null // 探测失败 → 端字段/身份缺省(调用方降级:保守保留)
|
|
186
|
+
}
|
|
187
|
+
}
|