@furongjun1999/dsh-memory 0.4.9 → 0.4.10
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/README.md +1 -1
- package/lib/lib/mdcg_client.d.ts +34 -0
- package/lib/lib/mdcg_client.js +40 -17
- package/md_cg/test_subproc_encoding.py +188 -0
- package/md_cg/units.py +4 -1
- package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
- package/package.json +1 -1
- package/src/lib/mdcg_client.ts +52 -17
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 让 AI Agent 拥有不可遗忘的记忆
|
|
2
2
|
|
|
3
|
-
**灵枢(Lingshu)** —— 高性能 · 无幻觉 · 多智能体适用的长期记忆系统(v0.4.
|
|
3
|
+
**灵枢(Lingshu)** —— 高性能 · 无幻觉 · 多智能体适用的长期记忆系统(v0.4.10)
|
|
4
4
|
|
|
5
5
|
[](https://awesome-dsh-plugin.com) [](https://www.dsh.so/artifact/dsh-memory-7) [](https://github.com/deepseek-ai/deepseek-harness/releases) [](#多-harness-接入按端分目录) [](package.json) [](LICENSE)
|
|
6
6
|
|
package/lib/lib/mdcg_client.d.ts
CHANGED
|
@@ -71,6 +71,40 @@ export interface MdcgOptions {
|
|
|
71
71
|
timeoutMs?: number;
|
|
72
72
|
maxRetryDelayMs?: number;
|
|
73
73
|
}
|
|
74
|
+
/** 子进程环境构造参数(`MdcgOptions` 中与环境相关的那半)。 */
|
|
75
|
+
export interface MdcgChildEnvOptions {
|
|
76
|
+
root: string;
|
|
77
|
+
surface?: 'kernel' | 'full';
|
|
78
|
+
tenant?: string;
|
|
79
|
+
clearance?: string;
|
|
80
|
+
actor?: string;
|
|
81
|
+
identity?: string;
|
|
82
|
+
env?: Record<string, string>;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* MCP 子进程环境:**唯一构造点**(导出即为了让机械守卫能断言它——
|
|
86
|
+
* 见 `test/python-utf8-mode.test.ts`)。勿在别处另拼 env。
|
|
87
|
+
*
|
|
88
|
+
* ⚠️ 两条编码注入是**硬约束**,不是可选项:
|
|
89
|
+
*
|
|
90
|
+
* ① `PYTHONIOENCODING=utf-8`(子进程**自身** stdio):Windows 下 piped 子进程默认
|
|
91
|
+
* gbk + surrogateescape,Node 写出的 UTF-8 中文会被解成孤立代理字符(\udcXX),
|
|
92
|
+
* md_cg 在落盘 / 回写 stdout 时抛 UnicodeEncodeError——中文记忆(主场景)全失败。
|
|
93
|
+
*
|
|
94
|
+
* ② `PYTHONUTF8=1`(子进程**后代**的默认 text 编码,PEP 540):`PYTHONIOENCODING`
|
|
95
|
+
* 会被后代继承(后代于是往管道写 UTF-8),但**文本解码口径不被继承**——后代读
|
|
96
|
+
* `subprocess.run(..., text=True)` 时取的是 locale(本机 cp936),于是
|
|
97
|
+
* 「子进程写 UTF-8、父进程按 gbk 读」→ 读线程崩死、诊断静默丢失。
|
|
98
|
+
* 2026-09-20 实证现场(`npm test` 周期复现,来源为子进程的后代代码单元):
|
|
99
|
+
* Exception in thread Thread-N (_readerthread):
|
|
100
|
+
* UnicodeDecodeError: 'gbk' codec can't decode byte 0x82 in position 181
|
|
101
|
+
* UTF-8 模式把默认 text 编码改为 UTF-8,读写两侧同口径(且解码结果正确,
|
|
102
|
+
* 而非 `errors="replace"` 那种替换字符)。
|
|
103
|
+
*
|
|
104
|
+
* 确定性对照实验(P1 复现 / P3 消除)见 `test/python-utf8-mode.test.ts`。
|
|
105
|
+
* `opts.env` 最后展开——显式覆盖优先。
|
|
106
|
+
*/
|
|
107
|
+
export declare function mdcgChildEnv(opts: MdcgChildEnvOptions): Record<string, string>;
|
|
74
108
|
/**
|
|
75
109
|
* 请求级**单元身份**(MCP 参数 `as_unit`)。
|
|
76
110
|
*
|
package/lib/lib/mdcg_client.js
CHANGED
|
@@ -37,6 +37,44 @@
|
|
|
37
37
|
import { LingshuBridge } from '../bridge.js';
|
|
38
38
|
import { pythonPathValue, runRoot } from './datapath.js';
|
|
39
39
|
const DEFAULT_ARGS = ['-m', 'md_cg.mcp_server'];
|
|
40
|
+
/**
|
|
41
|
+
* MCP 子进程环境:**唯一构造点**(导出即为了让机械守卫能断言它——
|
|
42
|
+
* 见 `test/python-utf8-mode.test.ts`)。勿在别处另拼 env。
|
|
43
|
+
*
|
|
44
|
+
* ⚠️ 两条编码注入是**硬约束**,不是可选项:
|
|
45
|
+
*
|
|
46
|
+
* ① `PYTHONIOENCODING=utf-8`(子进程**自身** stdio):Windows 下 piped 子进程默认
|
|
47
|
+
* gbk + surrogateescape,Node 写出的 UTF-8 中文会被解成孤立代理字符(\udcXX),
|
|
48
|
+
* md_cg 在落盘 / 回写 stdout 时抛 UnicodeEncodeError——中文记忆(主场景)全失败。
|
|
49
|
+
*
|
|
50
|
+
* ② `PYTHONUTF8=1`(子进程**后代**的默认 text 编码,PEP 540):`PYTHONIOENCODING`
|
|
51
|
+
* 会被后代继承(后代于是往管道写 UTF-8),但**文本解码口径不被继承**——后代读
|
|
52
|
+
* `subprocess.run(..., text=True)` 时取的是 locale(本机 cp936),于是
|
|
53
|
+
* 「子进程写 UTF-8、父进程按 gbk 读」→ 读线程崩死、诊断静默丢失。
|
|
54
|
+
* 2026-09-20 实证现场(`npm test` 周期复现,来源为子进程的后代代码单元):
|
|
55
|
+
* Exception in thread Thread-N (_readerthread):
|
|
56
|
+
* UnicodeDecodeError: 'gbk' codec can't decode byte 0x82 in position 181
|
|
57
|
+
* UTF-8 模式把默认 text 编码改为 UTF-8,读写两侧同口径(且解码结果正确,
|
|
58
|
+
* 而非 `errors="replace"` 那种替换字符)。
|
|
59
|
+
*
|
|
60
|
+
* 确定性对照实验(P1 复现 / P3 消除)见 `test/python-utf8-mode.test.ts`。
|
|
61
|
+
* `opts.env` 最后展开——显式覆盖优先。
|
|
62
|
+
*/
|
|
63
|
+
export function mdcgChildEnv(opts) {
|
|
64
|
+
return {
|
|
65
|
+
PYTHONIOENCODING: 'utf-8',
|
|
66
|
+
PYTHONUTF8: '1',
|
|
67
|
+
MDCG_ROOT: opts.root,
|
|
68
|
+
// 工具面必须是 full:写入通道 mdcg_remember 属细粒度工具(见 MdcgOptions.surface)。
|
|
69
|
+
MDCG_MCP_SURFACE: opts.surface ?? 'full',
|
|
70
|
+
MDCG_TENANT: opts.tenant ?? 'default',
|
|
71
|
+
MDCG_CLEARANCE: opts.clearance ?? 'private',
|
|
72
|
+
PYTHONPATH: pythonPathValue(),
|
|
73
|
+
...(opts.actor ? { MDCG_ACTOR: opts.actor } : {}),
|
|
74
|
+
...(opts.identity ? { MDCG_IDENTITY: opts.identity } : {}),
|
|
75
|
+
...(opts.env ?? {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
40
78
|
/** 认知图依据强度:这些 basis 视为「强依据」,可支撑 pass。
|
|
41
79
|
* 取值须在 md_cg.mdcg.VERIFICATION_BASIS 允许集内:
|
|
42
80
|
* compiler | test | measurement | formal_proof | data | other(other 不算强依据)。 */
|
|
@@ -68,23 +106,8 @@ export class MdcgClient {
|
|
|
68
106
|
// 模块解析不依赖 cwd:PYTHONPATH(pythonPathValue())已锚定随包 md_cg;
|
|
69
107
|
// opts.env 显式提供 PYTHONPATH 时完全接管(其展开在最后)。详见 datapath.runRoot()。
|
|
70
108
|
const cwd = opts.cwd ?? runRoot();
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// Node 写出的 UTF-8 中文会被解成孤立代理字符(\udcXX),md_cg 在落盘 /
|
|
74
|
-
// 回写 stdout 时抛 UnicodeEncodeError —— 中文记忆(本插件的主场景)全部失败。
|
|
75
|
-
// md_cg 自带测试(test_p2_mcp.py)与 test/bridge.test.ts 均以
|
|
76
|
-
// PYTHONIOENCODING=utf-8 启动子进程,此处对齐该约定;opts.env 可覆盖。
|
|
77
|
-
PYTHONIOENCODING: 'utf-8',
|
|
78
|
-
MDCG_ROOT: opts.root,
|
|
79
|
-
// 工具面必须是 full:写入通道 mdcg_remember 属细粒度工具(见 MdcgOptions.surface)。
|
|
80
|
-
MDCG_MCP_SURFACE: opts.surface ?? 'full',
|
|
81
|
-
MDCG_TENANT: opts.tenant ?? 'default',
|
|
82
|
-
MDCG_CLEARANCE: opts.clearance ?? 'private',
|
|
83
|
-
PYTHONPATH: pythonPathValue(),
|
|
84
|
-
...(opts.actor ? { MDCG_ACTOR: opts.actor } : {}),
|
|
85
|
-
...(opts.identity ? { MDCG_IDENTITY: opts.identity } : {}),
|
|
86
|
-
...(opts.env ?? {}),
|
|
87
|
-
};
|
|
109
|
+
// 编码与模块解析口径见 mdcgChildEnv() 头注(含 2026-09-20 读线程崩溃现场)。
|
|
110
|
+
const env = mdcgChildEnv(opts);
|
|
88
111
|
this.bridge = new LingshuBridge({
|
|
89
112
|
python: opts.python,
|
|
90
113
|
args: opts.args ?? DEFAULT_ARGS,
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""test_subproc_encoding.py · 子进程**文本解码口径**的机械守卫(第15条纪律的守卫面)
|
|
3
|
+
|
|
4
|
+
现场(2026-09-20 取证):桥的子进程被注入 `PYTHONIOENCODING=utf-8`,其**后代**因此往
|
|
5
|
+
管道写 UTF-8;但后代读子进程输出时默认 text 编码取自 locale(Windows=cp936),两侧
|
|
6
|
+
口径不一致 → 读线程崩死、子进程诊断静默丢失:
|
|
7
|
+
|
|
8
|
+
Exception in thread Thread-N (_readerthread):
|
|
9
|
+
UnicodeDecodeError: 'gbk' codec can't decode byte 0x82 in position 181
|
|
10
|
+
|
|
11
|
+
两道防线(本守卫管第二道):
|
|
12
|
+
① 运行期:桥子进程一律以 `PYTHONUTF8=1` 启动(PEP 540,默认 text 编码=utf-8);
|
|
13
|
+
见 `src/lib/mdcg_client.ts` 的 `mdcgChildEnv()` 与 `test/python-utf8-mode.test.ts`。
|
|
14
|
+
② 源码:**任何**文本模式子进程读取都必须显式声明 `encoding=`(配 `errors=`),
|
|
15
|
+
不依赖 locale——脚本/工具也可能被非桥路径启动,那时没有 ① 的保护。
|
|
16
|
+
|
|
17
|
+
判据(**AST 判定,非文本窗口**——2026-09-20 改进):以 `ast` 解析每个 git 跟踪的 .py,
|
|
18
|
+
对**真实的调用节点**判定:
|
|
19
|
+
· 子进程调用 = `subprocess.<run|Popen|check_output|call|check_call|getoutput|getstatusoutput>`
|
|
20
|
+
(含 `import subprocess as sp` 与 `from subprocess import run` 两种别名形态)
|
|
21
|
+
· 文本模式 = 调用处出现 `text=<非 False>` / `universal_newlines=<非 False>` / `encoding=`
|
|
22
|
+
· 违例 = 文本模式却无 `encoding=` 实参;`**kwargs` 透传且**该调用确有管道**
|
|
23
|
+
(`capture_output` 或 stdout/stderr=PIPE)而无 `encoding=` 报「不可判定」——口径无法
|
|
24
|
+
确认即不合格,须显式声明;`os.popen` 直接违例(该 API 无法声明编码)。
|
|
25
|
+
注:输出落 DEVNULL/日志文件且无管道的 `Popen` **不判**——它根本不经过解码器。
|
|
26
|
+
> 之所以不用文本窗口:窗口会把**字符串字面量与文档样例**当调用(守卫曾因此自伤,
|
|
27
|
+
> 把自身源码里的 `"os.popen("` 字串判为违例)——AST 只认语法意义上的调用,无此缺陷。
|
|
28
|
+
|
|
29
|
+
自检:被检查的文本模式调用点低于 FLOOR 视为失败——防止「扫描范围意外为空 → 假绿」。
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import ast
|
|
34
|
+
import os
|
|
35
|
+
import subprocess
|
|
36
|
+
import sys
|
|
37
|
+
|
|
38
|
+
FLOOR = 12 # 本仓文本模式子进程调用点的下限(低于它说明扫描面失效了)
|
|
39
|
+
SUB_FUNCS = {"run", "Popen", "check_output", "call", "check_call",
|
|
40
|
+
"getoutput", "getstatusoutput"}
|
|
41
|
+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# 生效条件:无入参;返回仓根下 git 跟踪的 .py 相对路径列表(git 不可用时返回 None,调用方据此跳过并说明)。
|
|
45
|
+
def _tracked_py() -> list[str] | None:
|
|
46
|
+
try:
|
|
47
|
+
r = subprocess.run(["git", "-C", ROOT, "ls-files", "*.py"],
|
|
48
|
+
capture_output=True, text=True,
|
|
49
|
+
encoding="utf-8", errors="replace")
|
|
50
|
+
except OSError:
|
|
51
|
+
return None
|
|
52
|
+
if r.returncode != 0:
|
|
53
|
+
return None
|
|
54
|
+
return [ln.strip() for ln in r.stdout.splitlines() if ln.strip()]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# 生效条件:v 为 ast 常量节点时按其布尔取反判定;非常量(变量/表达式)一律按 True 保守处理。
|
|
58
|
+
def _truthy(v: ast.AST | None) -> bool:
|
|
59
|
+
if isinstance(v, ast.Constant):
|
|
60
|
+
return bool(v.value)
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class _Scanner(ast.NodeVisitor):
|
|
65
|
+
"""单文件扫描器:先收别名导入,再按 AST 调用节点判文本模式与 encoding 声明。"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, rel: str, bad: list[str]) -> None:
|
|
68
|
+
self.rel = rel
|
|
69
|
+
self.bad = bad
|
|
70
|
+
self.sub_aliases: set[str] = set()
|
|
71
|
+
self.func_aliases: dict[str, str] = {}
|
|
72
|
+
self.checked = 0
|
|
73
|
+
|
|
74
|
+
def visit_Import(self, node: ast.Import) -> None:
|
|
75
|
+
for a in node.names:
|
|
76
|
+
if a.name == "subprocess":
|
|
77
|
+
self.sub_aliases.add(a.asname or "subprocess")
|
|
78
|
+
self.generic_visit(node)
|
|
79
|
+
|
|
80
|
+
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
|
81
|
+
if node.module == "subprocess":
|
|
82
|
+
for a in node.names:
|
|
83
|
+
if a.name in SUB_FUNCS:
|
|
84
|
+
self.func_aliases[a.asname or a.name] = a.name
|
|
85
|
+
self.generic_visit(node)
|
|
86
|
+
|
|
87
|
+
# 生效条件:node.func 为 subprocess.<func> / 别名 / os.popen 三种形态之一时返回 (kind, funcname),否则 None。
|
|
88
|
+
def _callee(self, node: ast.Call) -> tuple[str, str] | None:
|
|
89
|
+
f = node.func
|
|
90
|
+
if isinstance(f, ast.Attribute) and isinstance(f.value, ast.Name):
|
|
91
|
+
base = f.value.id
|
|
92
|
+
if base in self.sub_aliases and f.attr in SUB_FUNCS:
|
|
93
|
+
return ("subprocess", f.attr)
|
|
94
|
+
if base == "os" and f.attr == "popen":
|
|
95
|
+
return ("os", "popen")
|
|
96
|
+
if isinstance(f, ast.Name) and f.id in self.func_aliases:
|
|
97
|
+
return ("subprocess", self.func_aliases[f.id])
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
# 生效条件:kws 为调用处关键字实参表;capture_output 为真或 stdout/stderr 取 PIPE 时返回 True(该调用确有管道)。
|
|
101
|
+
@staticmethod
|
|
102
|
+
def _piped(kws: dict[str, ast.AST]) -> bool:
|
|
103
|
+
if "capture_output" in kws and _truthy(kws.get("capture_output")):
|
|
104
|
+
return True
|
|
105
|
+
for key in ("stdout", "stderr"):
|
|
106
|
+
v = kws.get(key)
|
|
107
|
+
if isinstance(v, ast.Attribute) and v.attr == "PIPE":
|
|
108
|
+
return True
|
|
109
|
+
if isinstance(v, ast.Name) and v.id == "PIPE":
|
|
110
|
+
return True
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
114
|
+
callee = self._callee(node)
|
|
115
|
+
if callee is not None:
|
|
116
|
+
kind, fname = callee
|
|
117
|
+
if kind == "os":
|
|
118
|
+
self.bad.append("%s:%d: os.popen(...)(该 API 无法声明编码,"
|
|
119
|
+
"改用 subprocess.run)" % (self.rel, node.lineno))
|
|
120
|
+
else:
|
|
121
|
+
kws = {k.arg: k.value for k in node.keywords if k.arg is not None}
|
|
122
|
+
has_star = any(k.arg is None for k in node.keywords)
|
|
123
|
+
text_mode = ("encoding" in kws
|
|
124
|
+
or ("text" in kws and _truthy(kws.get("text")))
|
|
125
|
+
or ("universal_newlines" in kws
|
|
126
|
+
and _truthy(kws.get("universal_newlines"))))
|
|
127
|
+
if text_mode:
|
|
128
|
+
self.checked += 1
|
|
129
|
+
if "encoding" not in kws:
|
|
130
|
+
self.bad.append(
|
|
131
|
+
"%s:%d: subprocess.%s(...) 文本模式未声明 encoding="
|
|
132
|
+
% (self.rel, node.lineno, fname))
|
|
133
|
+
elif has_star and self._piped(kws):
|
|
134
|
+
# **kwargs 透传且确有管道:口径不可判定(可能被调用方注入 text=True)→ 不合格。
|
|
135
|
+
# 输出落 DEVNULL/文件且无管道的调用不判——不经过解码器,无该缺陷面。
|
|
136
|
+
self.bad.append(
|
|
137
|
+
"%s:%d: subprocess.%s(..., **kwargs) 无法确认编码口径,"
|
|
138
|
+
"须显式声明 encoding=" % (self.rel, node.lineno, fname))
|
|
139
|
+
self.generic_visit(node)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# 生效条件:无入参;扫描 git 跟踪的 .py 返回 (违例列表, 受检文本模式调用点数, git 可用性)。
|
|
143
|
+
def scan() -> tuple[list[str], int, bool]:
|
|
144
|
+
files = _tracked_py()
|
|
145
|
+
if files is None:
|
|
146
|
+
return [], 0, False
|
|
147
|
+
bad: list[str] = []
|
|
148
|
+
checked = 0
|
|
149
|
+
for rel in files:
|
|
150
|
+
path = os.path.join(ROOT, rel)
|
|
151
|
+
try:
|
|
152
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
153
|
+
src = fh.read()
|
|
154
|
+
except OSError:
|
|
155
|
+
continue
|
|
156
|
+
try:
|
|
157
|
+
tree = ast.parse(src, filename=rel)
|
|
158
|
+
except SyntaxError:
|
|
159
|
+
bad.append("%s:0: 语法错误,无法 AST 判定(守卫拒绝在未解析文件上放行)" % rel)
|
|
160
|
+
continue
|
|
161
|
+
sc = _Scanner(rel, bad)
|
|
162
|
+
sc.visit(tree)
|
|
163
|
+
checked += sc.checked
|
|
164
|
+
return sorted(bad), checked, True
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def main() -> int:
|
|
168
|
+
bad, checked, git_ok = scan()
|
|
169
|
+
if not git_ok:
|
|
170
|
+
print("SKIP:git 不可用,无法枚举跟踪文件(本守卫需要 git)")
|
|
171
|
+
return 0
|
|
172
|
+
print("受检文本模式子进程调用点:%d(下限 %d)" % (checked, FLOOR))
|
|
173
|
+
if bad:
|
|
174
|
+
print("✖ 不合格的文本模式调用点 %d 处:" % len(bad))
|
|
175
|
+
for b in bad:
|
|
176
|
+
print(" ", b)
|
|
177
|
+
print("修法:补 encoding=\"utf-8\", errors=\"replace\"(不依赖 locale)")
|
|
178
|
+
return 1
|
|
179
|
+
if checked < FLOOR:
|
|
180
|
+
print("✖ 受检调用点 %d < 下限 %d——扫描面失效(文件枚举/判据坏了),"
|
|
181
|
+
"不得视为通过" % (checked, FLOOR))
|
|
182
|
+
return 1
|
|
183
|
+
print("✔ 全部调用点均显式声明 encoding=(子进程文本解码不依赖 locale)")
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
sys.exit(main())
|
package/md_cg/units.py
CHANGED
|
@@ -124,8 +124,11 @@ def _tasklist_row(pid):
|
|
|
124
124
|
按列精确比对,**不用子串包含**——子串会让 pid=441 被 4410 命中(假存活)。
|
|
125
125
|
"""
|
|
126
126
|
try:
|
|
127
|
+
# 显式 utf-8 + replace:只消费 ASCII 的 pid 列,但**不依赖 locale**——
|
|
128
|
+
# locale 口径与「后代写 UTF-8」不一致时读线程会崩(见 test_subproc_encoding.py)。
|
|
127
129
|
r = subprocess.run(["tasklist", "/FI", f"PID eq {pid}", "/NH", "/FO", "CSV"],
|
|
128
|
-
capture_output=True, text=True,
|
|
130
|
+
capture_output=True, text=True,
|
|
131
|
+
encoding="utf-8", errors="replace")
|
|
129
132
|
except OSError:
|
|
130
133
|
return None
|
|
131
134
|
for line in (r.stdout or "").splitlines():
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"初中数学知识点内容(按骨架填充)→小学数学知识点内容(按骨架填充)→高中数学知识点内容(按骨架填充)": 2,
|
|
6
6
|
"单卡:初中数学知识点内容(按骨架填充)": 2,
|
|
7
7
|
"单卡:小学数学知识点内容(按骨架填充)": 2,
|
|
8
|
-
"单卡:高中数学知识点内容(按骨架填充)":
|
|
8
|
+
"单卡:高中数学知识点内容(按骨架填充)": 244,
|
|
9
9
|
"小学科学→数学→初中物理": 9,
|
|
10
10
|
"单卡:小学科学": 9,
|
|
11
11
|
"单卡:数学": 9,
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
"单卡:数学物理方法": 9,
|
|
15
15
|
"单卡:理论力学": 9,
|
|
16
16
|
"单卡:物理学·综合": 9,
|
|
17
|
-
"单卡:打喷嚏(健康与身体·通识知识卡)":
|
|
18
|
-
"单卡:质数(数学·通识知识卡)":
|
|
19
|
-
"常见家电的功率(物理学·通识知识卡)→日食与月食的成因(天文学·通识知识卡)":
|
|
20
|
-
"单卡:常见家电的功率(物理学·通识知识卡)":
|
|
21
|
-
"单卡:日食与月食的成因(天文学·通识知识卡)":
|
|
22
|
-
"数学分析知识点内容(按骨架填充)→高中数学知识点内容(按骨架填充)→测度论与物理离散性":
|
|
23
|
-
"单卡:数学分析知识点内容(按骨架填充)":
|
|
24
|
-
"单卡:测度论与物理离散性":
|
|
25
|
-
"高中数学知识点内容(按骨架填充)→数学分析知识点内容(按骨架填充)→测度论与物理离散性":
|
|
17
|
+
"单卡:打喷嚏(健康与身体·通识知识卡)": 242,
|
|
18
|
+
"单卡:质数(数学·通识知识卡)": 242,
|
|
19
|
+
"常见家电的功率(物理学·通识知识卡)→日食与月食的成因(天文学·通识知识卡)": 242,
|
|
20
|
+
"单卡:常见家电的功率(物理学·通识知识卡)": 242,
|
|
21
|
+
"单卡:日食与月食的成因(天文学·通识知识卡)": 242,
|
|
22
|
+
"数学分析知识点内容(按骨架填充)→高中数学知识点内容(按骨架填充)→测度论与物理离散性": 116,
|
|
23
|
+
"单卡:数学分析知识点内容(按骨架填充)": 242,
|
|
24
|
+
"单卡:测度论与物理离散性": 242,
|
|
25
|
+
"高中数学知识点内容(按骨架填充)→数学分析知识点内容(按骨架填充)→测度论与物理离散性": 126,
|
|
26
26
|
"协议-知识飞轮→白箱测试定义→价值理论与AI对齐": 1,
|
|
27
27
|
"单卡:协议-知识飞轮": 1,
|
|
28
28
|
"单卡:白箱测试定义": 1,
|
package/package.json
CHANGED
package/src/lib/mdcg_client.ts
CHANGED
|
@@ -78,6 +78,56 @@ export interface MdcgOptions {
|
|
|
78
78
|
|
|
79
79
|
const DEFAULT_ARGS = ['-m', 'md_cg.mcp_server']
|
|
80
80
|
|
|
81
|
+
/** 子进程环境构造参数(`MdcgOptions` 中与环境相关的那半)。 */
|
|
82
|
+
export interface MdcgChildEnvOptions {
|
|
83
|
+
root: string
|
|
84
|
+
surface?: 'kernel' | 'full'
|
|
85
|
+
tenant?: string
|
|
86
|
+
clearance?: string
|
|
87
|
+
actor?: string
|
|
88
|
+
identity?: string
|
|
89
|
+
env?: Record<string, string>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* MCP 子进程环境:**唯一构造点**(导出即为了让机械守卫能断言它——
|
|
94
|
+
* 见 `test/python-utf8-mode.test.ts`)。勿在别处另拼 env。
|
|
95
|
+
*
|
|
96
|
+
* ⚠️ 两条编码注入是**硬约束**,不是可选项:
|
|
97
|
+
*
|
|
98
|
+
* ① `PYTHONIOENCODING=utf-8`(子进程**自身** stdio):Windows 下 piped 子进程默认
|
|
99
|
+
* gbk + surrogateescape,Node 写出的 UTF-8 中文会被解成孤立代理字符(\udcXX),
|
|
100
|
+
* md_cg 在落盘 / 回写 stdout 时抛 UnicodeEncodeError——中文记忆(主场景)全失败。
|
|
101
|
+
*
|
|
102
|
+
* ② `PYTHONUTF8=1`(子进程**后代**的默认 text 编码,PEP 540):`PYTHONIOENCODING`
|
|
103
|
+
* 会被后代继承(后代于是往管道写 UTF-8),但**文本解码口径不被继承**——后代读
|
|
104
|
+
* `subprocess.run(..., text=True)` 时取的是 locale(本机 cp936),于是
|
|
105
|
+
* 「子进程写 UTF-8、父进程按 gbk 读」→ 读线程崩死、诊断静默丢失。
|
|
106
|
+
* 2026-09-20 实证现场(`npm test` 周期复现,来源为子进程的后代代码单元):
|
|
107
|
+
* Exception in thread Thread-N (_readerthread):
|
|
108
|
+
* UnicodeDecodeError: 'gbk' codec can't decode byte 0x82 in position 181
|
|
109
|
+
* UTF-8 模式把默认 text 编码改为 UTF-8,读写两侧同口径(且解码结果正确,
|
|
110
|
+
* 而非 `errors="replace"` 那种替换字符)。
|
|
111
|
+
*
|
|
112
|
+
* 确定性对照实验(P1 复现 / P3 消除)见 `test/python-utf8-mode.test.ts`。
|
|
113
|
+
* `opts.env` 最后展开——显式覆盖优先。
|
|
114
|
+
*/
|
|
115
|
+
export function mdcgChildEnv(opts: MdcgChildEnvOptions): Record<string, string> {
|
|
116
|
+
return {
|
|
117
|
+
PYTHONIOENCODING: 'utf-8',
|
|
118
|
+
PYTHONUTF8: '1',
|
|
119
|
+
MDCG_ROOT: opts.root,
|
|
120
|
+
// 工具面必须是 full:写入通道 mdcg_remember 属细粒度工具(见 MdcgOptions.surface)。
|
|
121
|
+
MDCG_MCP_SURFACE: opts.surface ?? 'full',
|
|
122
|
+
MDCG_TENANT: opts.tenant ?? 'default',
|
|
123
|
+
MDCG_CLEARANCE: opts.clearance ?? 'private',
|
|
124
|
+
PYTHONPATH: pythonPathValue(),
|
|
125
|
+
...(opts.actor ? { MDCG_ACTOR: opts.actor } : {}),
|
|
126
|
+
...(opts.identity ? { MDCG_IDENTITY: opts.identity } : {}),
|
|
127
|
+
...(opts.env ?? {}),
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
81
131
|
/** 认知图依据强度:这些 basis 视为「强依据」,可支撑 pass。
|
|
82
132
|
* 取值须在 md_cg.mdcg.VERIFICATION_BASIS 允许集内:
|
|
83
133
|
* compiler | test | measurement | formal_proof | data | other(other 不算强依据)。 */
|
|
@@ -130,23 +180,8 @@ export class MdcgClient {
|
|
|
130
180
|
// 模块解析不依赖 cwd:PYTHONPATH(pythonPathValue())已锚定随包 md_cg;
|
|
131
181
|
// opts.env 显式提供 PYTHONPATH 时完全接管(其展开在最后)。详见 datapath.runRoot()。
|
|
132
182
|
const cwd = opts.cwd ?? runRoot()
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// Node 写出的 UTF-8 中文会被解成孤立代理字符(\udcXX),md_cg 在落盘 /
|
|
136
|
-
// 回写 stdout 时抛 UnicodeEncodeError —— 中文记忆(本插件的主场景)全部失败。
|
|
137
|
-
// md_cg 自带测试(test_p2_mcp.py)与 test/bridge.test.ts 均以
|
|
138
|
-
// PYTHONIOENCODING=utf-8 启动子进程,此处对齐该约定;opts.env 可覆盖。
|
|
139
|
-
PYTHONIOENCODING: 'utf-8',
|
|
140
|
-
MDCG_ROOT: opts.root,
|
|
141
|
-
// 工具面必须是 full:写入通道 mdcg_remember 属细粒度工具(见 MdcgOptions.surface)。
|
|
142
|
-
MDCG_MCP_SURFACE: opts.surface ?? 'full',
|
|
143
|
-
MDCG_TENANT: opts.tenant ?? 'default',
|
|
144
|
-
MDCG_CLEARANCE: opts.clearance ?? 'private',
|
|
145
|
-
PYTHONPATH: pythonPathValue(),
|
|
146
|
-
...(opts.actor ? { MDCG_ACTOR: opts.actor } : {}),
|
|
147
|
-
...(opts.identity ? { MDCG_IDENTITY: opts.identity } : {}),
|
|
148
|
-
...(opts.env ?? {}),
|
|
149
|
-
}
|
|
183
|
+
// 编码与模块解析口径见 mdcgChildEnv() 头注(含 2026-09-20 读线程崩溃现场)。
|
|
184
|
+
const env = mdcgChildEnv(opts)
|
|
150
185
|
this.bridge = new LingshuBridge({
|
|
151
186
|
python: opts.python,
|
|
152
187
|
args: opts.args ?? DEFAULT_ARGS,
|