@zhushanwen/pi-permission 1.1.0 → 1.3.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/package.json +2 -2
- package/skills/permission-ext-config/SKILL.md +5 -3
- package/src/__tests__/production.test.ts +25 -37
- package/src/ast/loader.ts +18 -3
- package/src/classifier/classifier.ts +2 -0
- package/src/config.ts +20 -0
- package/src/production.ts +12 -21
- package/src/types.ts +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-permission",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Pi permission extension — four permission modes (yolo/auto/approve/strict) with three-layer pipeline (AST + rules + AI classifier).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"tree-sitter-bash": "^0.25.1",
|
|
31
31
|
"web-tree-sitter": "^0.26.11",
|
|
32
|
-
"@zhushanwen/pi-llm-shared": "0.
|
|
32
|
+
"@zhushanwen/pi-llm-shared": "0.3.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"@earendil-works/pi-ai": "*",
|
|
@@ -48,6 +48,7 @@ interface ClassifierConfig {
|
|
|
48
48
|
timeout: number; // 超时秒,默认 90
|
|
49
49
|
autoApproveLowRisk: boolean; // 低风险自动放行,默认 true
|
|
50
50
|
autoDenyHighRisk: boolean; // 高风险转人工审批,默认 true
|
|
51
|
+
thinkingLevel: ModelThinkingLevel; // thinking 级别,默认 "off"(直接透传,llm-shared 映射为不传);"minimal"~"max" 透传
|
|
51
52
|
}
|
|
52
53
|
```
|
|
53
54
|
|
|
@@ -64,8 +65,8 @@ interface ClassifierConfig {
|
|
|
64
65
|
|
|
65
66
|
### classifier.model(只接受 string)
|
|
66
67
|
|
|
67
|
-
- `"auto"`(默认)→
|
|
68
|
-
- `"provider/model-id"`(如 `"zhipu/glm-4-flash"`)→
|
|
68
|
+
- `"auto"`(默认)→ permission 本地取 `ctx.modelRegistry.getAvailable()` 首个(不经过 llm-shared 的非精确 selector)
|
|
69
|
+
- `"provider/model-id"`(如 `"zhipu/glm-4-flash"`)→ 精确匹配(llm-shared `ModelSelector` 仅支持 ref)
|
|
69
70
|
- 传对象形式会被 console.warn 忽略,回落 auto
|
|
70
71
|
|
|
71
72
|
### auto 模式的风险判定(autoApproveLowRisk / autoDenyHighRisk)
|
|
@@ -91,7 +92,8 @@ AI classifier 输出三档风险,对应处理:
|
|
|
91
92
|
"model": "auto",
|
|
92
93
|
"timeout": 90,
|
|
93
94
|
"autoApproveLowRisk": true,
|
|
94
|
-
"autoDenyHighRisk": true
|
|
95
|
+
"autoDenyHighRisk": true,
|
|
96
|
+
"thinkingLevel": "off"
|
|
95
97
|
},
|
|
96
98
|
"userRules": []
|
|
97
99
|
}
|
|
@@ -91,16 +91,10 @@ beforeEach(() => {
|
|
|
91
91
|
|
|
92
92
|
// ──────────────────────── TC1/TC2: toSelector 映射 ────────────────────────
|
|
93
93
|
|
|
94
|
-
describe("toSelector
|
|
95
|
-
it("
|
|
96
|
-
expect(toSelector("auto")).toEqual({ type: "
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("TC2: 'provider/model-id' 映射为 ref", () => {
|
|
94
|
+
describe("toSelector(仅 ref 精确指定)", () => {
|
|
95
|
+
it("任意字符串都映射为 ref(auto 由 production resolveModel 层自行处理,不走 selector)", () => {
|
|
96
|
+
expect(toSelector("auto")).toEqual({ type: "ref", ref: "auto" });
|
|
100
97
|
expect(toSelector("anthropic/claude-sonnet")).toEqual({ type: "ref", ref: "anthropic/claude-sonnet" });
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("任意非 'auto' 字符串都视为 ref(含边角格式,由 resolveRef 判合法性)", () => {
|
|
104
98
|
expect(toSelector("openai/gpt-4o")).toEqual({ type: "ref", ref: "openai/gpt-4o" });
|
|
105
99
|
});
|
|
106
100
|
});
|
|
@@ -129,16 +123,17 @@ describe("createProductionClassifier", () => {
|
|
|
129
123
|
|
|
130
124
|
describe("resolveModel 装配(resolveModel + callLLM)", () => {
|
|
131
125
|
const CTX = { toolName: "bash", command: "ls", cwd: "/tmp" };
|
|
132
|
-
const
|
|
126
|
+
const CFG_AUTO = { enabled: true, model: "auto", timeout: 5, autoApproveLowRisk: false, autoDenyHighRisk: true };
|
|
127
|
+
const CFG_REF = { enabled: true, model: "test-co/model-a", timeout: 5, autoApproveLowRisk: false, autoDenyHighRisk: true };
|
|
133
128
|
|
|
134
|
-
it("TC3:
|
|
135
|
-
|
|
136
|
-
const ctx = makeMockCtx();
|
|
129
|
+
it("TC3: auto 且 getAvailable 空 → resolveModel null → fail-closed ask(不触达 callLLM)", async () => {
|
|
130
|
+
const ctx = makeMockCtx({ getAvailable: () => [] });
|
|
137
131
|
const classifier = createProductionClassifier(ctx);
|
|
138
|
-
const result = await classifier.classifyRisk(CTX,
|
|
132
|
+
const result = await classifier.classifyRisk(CTX, CFG_AUTO);
|
|
139
133
|
expect(result.outcome).toBe("ask");
|
|
140
134
|
expect(result.confidence).toBe(0);
|
|
141
|
-
//
|
|
135
|
+
// auto 是 permission 本地解析,不经过 llm-shared resolveModel
|
|
136
|
+
expect(resolveModelShared).not.toHaveBeenCalled();
|
|
142
137
|
expect(callLLMShared).not.toHaveBeenCalled();
|
|
143
138
|
});
|
|
144
139
|
|
|
@@ -154,16 +149,14 @@ describe("resolveModel 装配(resolveModel + callLLM)", () => {
|
|
|
154
149
|
expect(source).toContain('from "@zhushanwen/pi-llm-shared"');
|
|
155
150
|
});
|
|
156
151
|
|
|
157
|
-
it("TC4(装配): resolveModel 命中 → callLLM 收到 model → ok:true 正常分类(收口链路通)", async () => {
|
|
158
|
-
vi.mocked(resolveModelShared).
|
|
159
|
-
selector.type === "scoped" ? MOCK_MODEL_A : null,
|
|
160
|
-
);
|
|
152
|
+
it("TC4(装配): ref resolveModel 命中 → callLLM 收到 model → ok:true 正常分类(收口链路通)", async () => {
|
|
153
|
+
vi.mocked(resolveModelShared).mockReturnValue(MOCK_MODEL_A);
|
|
161
154
|
vi.mocked(callLLMShared).mockResolvedValue({
|
|
162
155
|
ok: true,
|
|
163
156
|
content: '{"outcome":"allow","risk_level":"low","reasoning":"safe","confidence":0.9}',
|
|
164
157
|
});
|
|
165
158
|
const classifier = createProductionClassifier(makeMockCtx());
|
|
166
|
-
const result = await classifier.classifyRisk(CTX,
|
|
159
|
+
const result = await classifier.classifyRisk(CTX, CFG_REF);
|
|
167
160
|
expect(result.outcome).toBe("allow");
|
|
168
161
|
// 关键:resolveModel 返回 model 后,callLLM 被调用且收到该 model(收口链路 resolve→callLLM 通)
|
|
169
162
|
expect(callLLMShared).toHaveBeenCalledOnce();
|
|
@@ -172,19 +165,18 @@ describe("resolveModel 装配(resolveModel + callLLM)", () => {
|
|
|
172
165
|
expect(opts.model).toBe(MOCK_MODEL_A);
|
|
173
166
|
expect(opts.messages).toHaveLength(1);
|
|
174
167
|
expect(opts.systemPrompt.length).toBeGreaterThan(0);
|
|
175
|
-
//
|
|
168
|
+
// ref 精确指定只调 1 次 resolveModel
|
|
176
169
|
expect(vi.mocked(resolveModelShared)).toHaveBeenCalledTimes(1);
|
|
170
|
+
expect(vi.mocked(resolveModelShared)).toHaveBeenCalledWith(expect.anything(), { type: "ref", ref: "test-co/model-a" });
|
|
177
171
|
});
|
|
178
172
|
|
|
179
|
-
it("TC4b: resolveModel
|
|
180
|
-
vi.mocked(resolveModelShared).
|
|
181
|
-
selector.type === "scoped" ? MOCK_MODEL_A : null,
|
|
182
|
-
);
|
|
173
|
+
it("TC4b: ref resolveModel 成功 → onLog 输出 '[pi-permission] classifier: using model <modelId>'(契约 C2,R2 验收前提)", async () => {
|
|
174
|
+
vi.mocked(resolveModelShared).mockReturnValue(MOCK_MODEL_A);
|
|
183
175
|
vi.mocked(callLLMShared).mockResolvedValue({ ok: true, content: "x" });
|
|
184
176
|
const classifier = createProductionClassifier(makeMockCtx());
|
|
185
177
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
186
178
|
try {
|
|
187
|
-
await classifier.classifyRisk(CTX,
|
|
179
|
+
await classifier.classifyRisk(CTX, CFG_REF);
|
|
188
180
|
// onLog 实装为 console.warn(production.ts),成功路径日志在 LLM 调用前输出
|
|
189
181
|
expect(warnSpy).toHaveBeenCalledWith("[pi-permission] classifier: using model model-a");
|
|
190
182
|
} finally {
|
|
@@ -196,26 +188,22 @@ describe("resolveModel 装配(resolveModel + callLLM)", () => {
|
|
|
196
188
|
vi.mocked(resolveModelShared).mockReturnValue(MOCK_MODEL_A);
|
|
197
189
|
vi.mocked(callLLMShared).mockResolvedValue({ ok: false, error: "token expired", recoverable: true });
|
|
198
190
|
const classifier = createProductionClassifier(makeMockCtx());
|
|
199
|
-
const result = await classifier.classifyRisk(CTX,
|
|
191
|
+
const result = await classifier.classifyRisk(CTX, CFG_REF);
|
|
200
192
|
expect(result.outcome).toBe("ask");
|
|
201
193
|
expect(result.confidence).toBe(0);
|
|
202
194
|
expect(callLLMShared).toHaveBeenCalledOnce();
|
|
203
195
|
});
|
|
204
196
|
|
|
205
|
-
it("TC7:
|
|
206
|
-
|
|
207
|
-
vi.mocked(resolveModelShared).mockImplementation((_ctx, selector) =>
|
|
208
|
-
selector.type === "available" ? MOCK_MODEL_A : null,
|
|
209
|
-
);
|
|
197
|
+
it("TC7: auto 直接取 getAvailable 首个(permission 本地兼容,不经过非精确 ModelSelector)", async () => {
|
|
198
|
+
const ctx = makeMockCtx({ getAvailable: () => [MOCK_MODEL_A] });
|
|
210
199
|
vi.mocked(callLLMShared).mockResolvedValue({ ok: true, content: "x" });
|
|
211
|
-
const classifier = createProductionClassifier(
|
|
212
|
-
const result = await classifier.classifyRisk(CTX,
|
|
213
|
-
// fallback 命中 available → callLLM 收到 available model(非 scoped 的 null 直接返回)
|
|
200
|
+
const classifier = createProductionClassifier(ctx);
|
|
201
|
+
const result = await classifier.classifyRisk(CTX, CFG_AUTO);
|
|
214
202
|
expect(callLLMShared).toHaveBeenCalledOnce();
|
|
215
203
|
const opts = vi.mocked(callLLMShared).mock.calls[0]![1];
|
|
216
204
|
expect(opts.model).toBe(MOCK_MODEL_A);
|
|
217
|
-
//
|
|
218
|
-
expect(
|
|
205
|
+
// auto 不经过 llm-shared resolveModel
|
|
206
|
+
expect(resolveModelShared).not.toHaveBeenCalled();
|
|
219
207
|
// callLLM ok:true 但 content 非 JSON → parser fallback ask(链路已证明 resolve→callLLM 非 null)
|
|
220
208
|
expect(result.outcome).toBe("ask");
|
|
221
209
|
});
|
package/src/ast/loader.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { createRequire } from "node:module";
|
|
23
23
|
import { existsSync } from "node:fs";
|
|
24
24
|
import { dirname, join } from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
25
26
|
|
|
26
27
|
import { Language, Parser } from "web-tree-sitter";
|
|
27
28
|
|
|
@@ -45,9 +46,25 @@ export interface WasmPaths {
|
|
|
45
46
|
* 所以这里只定位 .js 所在目录,不硬编码 wasm 文件名(跨版本健壮)。
|
|
46
47
|
*
|
|
47
48
|
* 任一不存在则 throw(fail-loud,由 getBashParser 捕获后 fail-closed)。
|
|
49
|
+
*
|
|
50
|
+
* 双模式(R1,bundle 适配):builtin extension 经 esbuild bundle 成自包含 index.js 后,
|
|
51
|
+
* web-tree-sitter 被 inline、staged 产物无 node_modules,nodeRequire.resolve 必然失败。
|
|
52
|
+
* 故优先尝试 bundle 模式 —— 用 fileURLToPath(import.meta.url) 定位 index.js 所在目录,
|
|
53
|
+
* 查同目录的 tree-sitter-bash.wasm;命中则返回(wasm 由 bundle-extensions.mjs 拷至此)。
|
|
54
|
+
* 未命中则 fallback 到 nodeRequire.resolve(开发期 dev-link,源码 node_modules 完整)。
|
|
48
55
|
*/
|
|
49
56
|
export function resolveWasmPaths(): WasmPaths {
|
|
50
|
-
//
|
|
57
|
+
// bundle 模式:bundle 后 wasm 与 index.js 同目录(bundle-extensions.mjs 拷贝至此)。
|
|
58
|
+
// 查两个 wasm 都存在才返回,避免不对称 guard(bash.wasm 在但 web-tree-sitter.wasm
|
|
59
|
+
// 缺失时 emscripten locateFile 会失败,虽 fail-closed 但不如入口 guard 显式拦截)。
|
|
60
|
+
const bundleDir = dirname(fileURLToPath(import.meta.url));
|
|
61
|
+
const bundleBashWasm = join(bundleDir, "tree-sitter-bash.wasm");
|
|
62
|
+
const bundleRuntimeWasm = join(bundleDir, "web-tree-sitter.wasm");
|
|
63
|
+
if (existsSync(bundleBashWasm) && existsSync(bundleRuntimeWasm)) {
|
|
64
|
+
return { bashWasmPath: bundleBashWasm, runtimeWasmDir: bundleDir };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// fallback:开发期从 node_modules 解析(dev-link,源码目录完整)
|
|
51
68
|
const bashPkgDir = dirname(nodeRequire.resolve("tree-sitter-bash/package.json"));
|
|
52
69
|
const bashWasmPath = join(bashPkgDir, "tree-sitter-bash.wasm");
|
|
53
70
|
if (!existsSync(bashWasmPath)) {
|
|
@@ -56,8 +73,6 @@ export function resolveWasmPaths(): WasmPaths {
|
|
|
56
73
|
);
|
|
57
74
|
}
|
|
58
75
|
|
|
59
|
-
// 2. 运行时 wasm 目录 —— web-tree-sitter 的 .js / .wasm 同目录
|
|
60
|
-
// 用 resolve("web-tree-sitter") 定位 .js 入口,取其 dirname。
|
|
61
76
|
const runtimeEntry = nodeRequire.resolve("web-tree-sitter");
|
|
62
77
|
const runtimeWasmDir = dirname(runtimeEntry);
|
|
63
78
|
if (!existsSync(runtimeWasmDir)) {
|
|
@@ -157,6 +157,8 @@ export function createClassifier(deps: ClassifierDeps): {
|
|
|
157
157
|
messages: buildMessages(ctx),
|
|
158
158
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
159
159
|
...(signal !== undefined ? { signal } : {}),
|
|
160
|
+
// thinkingLevel 直接透传(含 "off");llm-shared 内部会把 "off" 映射为不传 reasoning(provider 默认)
|
|
161
|
+
reasoning: config.thinkingLevel,
|
|
160
162
|
});
|
|
161
163
|
|
|
162
164
|
// 3. 等待 callLLM + 外层超时/中止兜底(provider 不支持 timeoutMs/signal 时防永挂)
|
package/src/config.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
18
18
|
import { dirname, join } from "node:path";
|
|
19
19
|
|
|
20
|
+
import type { ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
20
21
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
21
22
|
import {
|
|
22
23
|
clearConfigCache,
|
|
@@ -50,6 +51,20 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
50
51
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
const THINKING_LEVELS: ReadonlySet<string> = new Set([
|
|
55
|
+
"off",
|
|
56
|
+
"minimal",
|
|
57
|
+
"low",
|
|
58
|
+
"medium",
|
|
59
|
+
"high",
|
|
60
|
+
"xhigh",
|
|
61
|
+
"max",
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
function isModelThinkingLevel(value: unknown): value is ModelThinkingLevel {
|
|
65
|
+
return typeof value === "string" && THINKING_LEVELS.has(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
53
68
|
function normalizeClassifierConfig(raw: unknown): ClassifierConfig {
|
|
54
69
|
const record = isPlainObject(raw) ? raw : {};
|
|
55
70
|
const timeout = Number(record.timeout);
|
|
@@ -58,12 +73,17 @@ function normalizeClassifierConfig(raw: unknown): ClassifierConfig {
|
|
|
58
73
|
if (record.model !== undefined && !(typeof record.model === "string" && record.model.length > 0)) {
|
|
59
74
|
console.warn("[pi-permission] Ignoring invalid classifier.model (expected string 'auto' or 'provider/model-id'), using default auto");
|
|
60
75
|
}
|
|
76
|
+
// thinkingLevel 校验:合法值为 'off'|'minimal'|'low'|'medium'|'high'|'xhigh'|'max'
|
|
77
|
+
const thinkingLevel = isModelThinkingLevel(record.thinkingLevel)
|
|
78
|
+
? record.thinkingLevel
|
|
79
|
+
: DEFAULT_CLASSIFIER_CONFIG.thinkingLevel;
|
|
61
80
|
return {
|
|
62
81
|
enabled: record.enabled !== false,
|
|
63
82
|
model: typeof record.model === "string" && record.model.length > 0 ? record.model : DEFAULT_CLASSIFIER_CONFIG.model,
|
|
64
83
|
timeout: Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_CLASSIFIER_CONFIG.timeout,
|
|
65
84
|
autoApproveLowRisk: record.autoApproveLowRisk !== false,
|
|
66
85
|
autoDenyHighRisk: record.autoDenyHighRisk !== false,
|
|
86
|
+
thinkingLevel,
|
|
67
87
|
};
|
|
68
88
|
}
|
|
69
89
|
|
package/src/production.ts
CHANGED
|
@@ -27,20 +27,15 @@ import { createClassifier } from "./classifier/index.js";
|
|
|
27
27
|
import type { CheckPermissionDeps } from "./pipeline.js";
|
|
28
28
|
import { getDefaultRules, matchRulesForArgv } from "./rules/index.js";
|
|
29
29
|
|
|
30
|
-
// ──────────────────────── toSelector
|
|
30
|
+
// ──────────────────────── toSelector(仅 ref 精确指定) ────────────────────────
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* 把 ClassifierConfig.model 字符串映射为 llm-shared ModelSelector。
|
|
34
34
|
*
|
|
35
|
-
* -
|
|
36
|
-
*
|
|
37
|
-
* - 其余视为 'provider/model-id' → { type: 'ref', ref: modelSpec }:精确匹配。
|
|
38
|
-
*
|
|
39
|
-
* 注:toSelector 只负责 selector 映射;'auto' 的向后兼容(scoped 空 fallback available)
|
|
40
|
-
* 在下方 resolveModel 注入层处理(CL-scoped-fallback),toSelector 本身不感知 fallback。
|
|
35
|
+
* llm-shared 只支持 ref 精确指定;permission 的 "auto" 在 resolveModel 注入层自行处理,
|
|
36
|
+
* 不通过 ModelSelector 表达。
|
|
41
37
|
*/
|
|
42
38
|
export function toSelector(modelSpec: string): ModelSelector {
|
|
43
|
-
if (modelSpec === "auto") return { type: "scoped" };
|
|
44
39
|
return { type: "ref", ref: modelSpec };
|
|
45
40
|
}
|
|
46
41
|
|
|
@@ -49,12 +44,11 @@ export function toSelector(modelSpec: string): ModelSelector {
|
|
|
49
44
|
/**
|
|
50
45
|
* 创建生产环境 AI Classifier。
|
|
51
46
|
*
|
|
52
|
-
* resolveModel
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* 4. model 仍 null → 返回 null(fail-closed)
|
|
47
|
+
* resolveModel 注入:
|
|
48
|
+
* - config.model === "auto":直接取 ctx.modelRegistry.getAvailable()[0](permission 本地
|
|
49
|
+
* 向后兼容行为,不经过 llm-shared 的非精确 selector)。
|
|
50
|
+
* - 其余:toSelector(config.model) → resolveModelShared(ctx, selector)(仅 ref 精确匹配)。
|
|
51
|
+
* - model 仍 null → 返回 null(fail-closed)。
|
|
58
52
|
*
|
|
59
53
|
* callLLM 注入(C1a):闭包捕获 ctx,把 llm-shared callLLM 绑定为 classifier 的 LLM 调用。
|
|
60
54
|
* 凭证获取(getApiKeyAndHeaders)在 callLLM 内部完成(C1a 收口后不再在 resolveModel 层预检)。
|
|
@@ -64,14 +58,11 @@ export function createProductionClassifier(ctx: ExtensionContext): {
|
|
|
64
58
|
} {
|
|
65
59
|
return createClassifier({
|
|
66
60
|
resolveModel: async (config) => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// 保证「有 apiKey provider 但没配 enabledModels」的用户不退化(旧 auto 行为)。
|
|
71
|
-
if (!model && selector.type === "scoped") {
|
|
72
|
-
model = resolveModelShared(ctx, { type: "available" });
|
|
61
|
+
if (config.model === "auto") {
|
|
62
|
+
const available = ctx.modelRegistry.getAvailable();
|
|
63
|
+
return available.length > 0 ? available[0] : null;
|
|
73
64
|
}
|
|
74
|
-
return model;
|
|
65
|
+
return resolveModelShared(ctx, toSelector(config.model));
|
|
75
66
|
},
|
|
76
67
|
callLLM: (opts) => callLLM(ctx, opts),
|
|
77
68
|
onLog: (msg: string) => console.warn(msg),
|
package/src/types.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* 对应 slice plan 的 DM1-DM5 数据模型。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
9
|
+
|
|
8
10
|
// ──────────────────────── DM1: PermissionMode ────────────────────────
|
|
9
11
|
|
|
10
12
|
/** 四档权限模式,按严格等级排序(yolo 最低,strict 最高) */
|
|
@@ -96,6 +98,12 @@ export interface ClassifierConfig {
|
|
|
96
98
|
autoApproveLowRisk: boolean;
|
|
97
99
|
/** 高风险是否自动拦截(转人工审批) */
|
|
98
100
|
autoDenyHighRisk: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* 标题生成 LLM 的 thinking 级别(pi 的 ModelThinkingLevel,THINKING_ORDER SSOT)。
|
|
103
|
+
* 默认 "off":不传 pi-ai reasoning(provider 默认行为);
|
|
104
|
+
* "minimal"~"max" 透传给 SimpleStreamOptions.reasoning(provider 不支持时静默忽略)。
|
|
105
|
+
*/
|
|
106
|
+
thinkingLevel: ModelThinkingLevel;
|
|
99
107
|
}
|
|
100
108
|
|
|
101
109
|
// ──────────────────────── DM5: PermissionConfig ────────────────────────
|
|
@@ -120,6 +128,7 @@ export const DEFAULT_CLASSIFIER_CONFIG: ClassifierConfig = {
|
|
|
120
128
|
timeout: 90,
|
|
121
129
|
autoApproveLowRisk: true,
|
|
122
130
|
autoDenyHighRisk: true,
|
|
131
|
+
thinkingLevel: "off",
|
|
123
132
|
};
|
|
124
133
|
|
|
125
134
|
export const DEFAULT_CONFIG: PermissionConfig = {
|