@cjhyy/code-shell-core 0.6.0-rc.9 → 0.7.0-beta.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/THIRD_PARTY_NOTICES.md +206 -0
- package/dist/automation/scheduler.d.ts +13 -7
- package/dist/automation/scheduler.js +116 -37
- package/dist/capability-control/service.d.ts +2 -0
- package/dist/capability-control/service.js +4 -2
- package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
- package/dist/cc-orchestrator/agent-adapter.js +7 -1
- package/dist/cc-orchestrator/codex-session-history.js +1 -1
- package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
- package/dist/cc-orchestrator/cwd-normalize.js +19 -0
- package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
- package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
- package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
- package/dist/cc-orchestrator/external-agent-driver.js +102 -51
- package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
- package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
- package/dist/cc-orchestrator/session-history.js +2 -2
- package/dist/cli/agent-server-stdio.js +9 -2
- package/dist/context/compaction.d.ts +8 -1
- package/dist/context/compaction.js +49 -4
- package/dist/context/manager.d.ts +16 -2
- package/dist/context/manager.js +103 -19
- package/dist/credentials/access.d.ts +56 -0
- package/dist/credentials/access.js +183 -0
- package/dist/credentials/index.d.ts +1 -0
- package/dist/credentials/index.js +1 -0
- package/dist/credentials/inject-credential-tool.d.ts +3 -1
- package/dist/credentials/inject-credential-tool.js +30 -11
- package/dist/credentials/types.d.ts +2 -2
- package/dist/credentials/use-credential-tool.d.ts +9 -1
- package/dist/credentials/use-credential-tool.js +58 -45
- package/dist/engine/engine.d.ts +17 -1
- package/dist/engine/engine.js +247 -89
- package/dist/engine/image-policy.d.ts +6 -0
- package/dist/engine/image-policy.js +17 -6
- package/dist/engine/input-attachments.d.ts +13 -0
- package/dist/engine/input-attachments.js +255 -0
- package/dist/engine/model-facade.d.ts +5 -2
- package/dist/engine/model-facade.js +4 -4
- package/dist/engine/parse-task.d.ts +10 -0
- package/dist/engine/parse-task.js +5 -0
- package/dist/engine/streaming-tool-queue.d.ts +11 -7
- package/dist/engine/streaming-tool-queue.js +11 -7
- package/dist/engine/turn-loop.d.ts +7 -1
- package/dist/engine/turn-loop.js +117 -27
- package/dist/engine/types.d.ts +8 -0
- package/dist/git/worktree/crud.d.ts +69 -0
- package/dist/git/worktree/crud.js +206 -0
- package/dist/git/worktree/diff.d.ts +14 -0
- package/dist/git/worktree/diff.js +82 -0
- package/dist/git/worktree/git-exec.d.ts +7 -0
- package/dist/git/worktree/git-exec.js +51 -0
- package/dist/git/worktree/index.d.ts +5 -0
- package/dist/git/worktree/index.js +5 -0
- package/dist/git/worktree/query.d.ts +42 -0
- package/dist/git/worktree/query.js +121 -0
- package/dist/git/worktree/slug.d.ts +11 -0
- package/dist/git/worktree/slug.js +58 -0
- package/dist/git/worktree.d.ts +1 -84
- package/dist/git/worktree.js +5 -230
- package/dist/index.d.ts +31 -28
- package/dist/index.js +27 -25
- package/dist/logging/logger.js +6 -6
- package/dist/logging/sanitize-messages.d.ts +10 -2
- package/dist/logging/sanitize-messages.js +21 -6
- package/dist/plugins/installer/checkUpdate.d.ts +4 -1
- package/dist/plugins/installer/checkUpdate.js +4 -2
- package/dist/plugins/installer/install.js +2 -0
- package/dist/plugins/installer/installFromSource.js +9 -1
- package/dist/plugins/installer/parseSource.d.ts +4 -1
- package/dist/plugins/installer/parseSource.js +28 -10
- package/dist/plugins/installer/sourcePath.d.ts +9 -0
- package/dist/plugins/installer/sourcePath.js +50 -0
- package/dist/plugins/installer/update.d.ts +4 -1
- package/dist/plugins/installer/update.js +5 -3
- package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
- package/dist/plugins/parseMarketplaceInput.js +4 -3
- package/dist/plugins/pluginInstaller.js +24 -22
- package/dist/preset/index.d.ts +1 -0
- package/dist/preset/index.js +16 -9
- package/dist/prompt/sections/base.md +1 -1
- package/dist/protocol/chat-session-manager.d.ts +2 -0
- package/dist/protocol/chat-session-manager.js +38 -2
- package/dist/protocol/chat-session.d.ts +3 -0
- package/dist/protocol/chat-session.js +1 -0
- package/dist/protocol/client.d.ts +9 -4
- package/dist/protocol/client.js +18 -1
- package/dist/protocol/server.d.ts +30 -0
- package/dist/protocol/server.js +246 -46
- package/dist/protocol/types.d.ts +47 -0
- package/dist/protocol/types.js +6 -0
- package/dist/run/FileRunStore.js +10 -1
- package/dist/run/Heartbeat.js +12 -0
- package/dist/run/RunApprovalBackend.d.ts +3 -0
- package/dist/run/RunApprovalBackend.js +41 -6
- package/dist/run/RunLock.js +2 -0
- package/dist/run/RunManager.d.ts +2 -0
- package/dist/run/RunManager.js +64 -24
- package/dist/run/ids.d.ts +2 -0
- package/dist/run/ids.js +23 -0
- package/dist/runtime/background-shell.d.ts +1 -0
- package/dist/runtime/background-shell.js +23 -13
- package/dist/runtime/spawn-common.js +10 -0
- package/dist/services/auto-dream.d.ts +15 -4
- package/dist/services/auto-dream.js +20 -20
- package/dist/services/dream-consolidation.d.ts +2 -3
- package/dist/services/dream-consolidation.js +65 -15
- package/dist/services/extract-memories.d.ts +14 -5
- package/dist/services/extract-memories.js +20 -3
- package/dist/services/global-dream-promotion.d.ts +23 -0
- package/dist/services/global-dream-promotion.js +112 -0
- package/dist/services/memory-orchestrator.js +347 -34
- package/dist/session/memory.d.ts +61 -18
- package/dist/session/memory.js +342 -79
- package/dist/session/session-manager.d.ts +35 -1
- package/dist/session/session-manager.js +190 -3
- package/dist/session/transcript.d.ts +1 -1
- package/dist/session/transcript.js +17 -5
- package/dist/settings/manager.d.ts +1 -0
- package/dist/settings/manager.js +87 -37
- package/dist/settings/schema-export.d.ts +2 -3
- package/dist/settings/schema-export.js +2 -3
- package/dist/settings/schema.d.ts +21 -0
- package/dist/settings/schema.js +12 -0
- package/dist/skills/scanner.d.ts +3 -2
- package/dist/skills/scanner.js +12 -9
- package/dist/tool-system/builtin/background-jobs.d.ts +10 -1
- package/dist/tool-system/builtin/background-jobs.js +12 -1
- package/dist/tool-system/builtin/background-work.d.ts +17 -18
- package/dist/tool-system/builtin/background-work.js +51 -5
- package/dist/tool-system/builtin/config.d.ts +2 -1
- package/dist/tool-system/builtin/config.js +16 -11
- package/dist/tool-system/builtin/drive-claude-code.d.ts +16 -2
- package/dist/tool-system/builtin/drive-claude-code.js +301 -47
- package/dist/tool-system/builtin/edit.js +5 -2
- package/dist/tool-system/builtin/generate-video.d.ts +1 -0
- package/dist/tool-system/builtin/generate-video.js +13 -4
- package/dist/tool-system/builtin/index.d.ts +8 -3
- package/dist/tool-system/builtin/index.js +37 -21
- package/dist/tool-system/builtin/lsp.d.ts +2 -1
- package/dist/tool-system/builtin/lsp.js +6 -3
- package/dist/tool-system/builtin/memory.js +40 -8
- package/dist/tool-system/builtin/notebook-edit.js +5 -2
- package/dist/tool-system/builtin/powershell.d.ts +5 -2
- package/dist/tool-system/builtin/powershell.js +11 -7
- package/dist/tool-system/builtin/read.js +118 -6
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/view-image.js +109 -19
- package/dist/tool-system/builtin/worktree.d.ts +4 -4
- package/dist/tool-system/builtin/worktree.js +297 -74
- package/dist/tool-system/builtin/write.js +5 -3
- package/dist/tool-system/context.d.ts +23 -1
- package/dist/tool-system/executor.d.ts +1 -5
- package/dist/tool-system/executor.js +94 -115
- package/dist/tool-system/mcp-manager.d.ts +18 -5
- package/dist/tool-system/mcp-manager.js +140 -74
- package/dist/tool-system/path-policy.d.ts +2 -0
- package/dist/tool-system/path-policy.js +41 -12
- package/dist/tool-system/permission.d.ts +28 -7
- package/dist/tool-system/permission.js +130 -49
- package/dist/tool-system/registry.js +11 -4
- package/dist/tool-system/tool-result-redaction.d.ts +7 -0
- package/dist/tool-system/tool-result-redaction.js +48 -0
- package/dist/tool-system/workspace-bridge.d.ts +11 -0
- package/dist/tool-system/workspace-bridge.js +1 -0
- package/dist/types.d.ts +55 -4
- package/dist/utils/toolDisplay.js +1 -1
- package/package.json +4 -3
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { readToolDef, readTool } from "./read.js";
|
|
5
5
|
import { writeToolDef, writeTool } from "./write.js";
|
|
6
|
-
import { generateImageToolDef, generateImageTool, isGenerateImageAvailable } from "./generate-image.js";
|
|
6
|
+
import { generateImageToolDef, generateImageTool, isGenerateImageAvailable, } from "./generate-image.js";
|
|
7
7
|
import { editModelCatalogToolDef, editModelCatalogTool } from "./edit-model-catalog.js";
|
|
8
|
-
import { generateVideoToolDef, generateVideoTool, isGenerateVideoAvailable } from "./generate-video.js";
|
|
8
|
+
import { generateVideoToolDef, generateVideoTool, isGenerateVideoAvailable, } from "./generate-video.js";
|
|
9
9
|
import { viewImageToolDef, viewImageTool } from "./view-image.js";
|
|
10
10
|
import { editToolDef, editTool } from "./edit.js";
|
|
11
11
|
import { applyPatchToolDef, applyPatchTool } from "./apply-patch/index.js";
|
|
@@ -16,19 +16,19 @@ import { webSearchToolDef, webSearchTool, isWebSearchAvailable } from "./web-sea
|
|
|
16
16
|
import { webFetchToolDef, webFetchTool } from "./web-fetch.js";
|
|
17
17
|
import { askUserToolDef, askUserTool } from "./ask-user.js";
|
|
18
18
|
import { agentToolDef, agentTool, agentStatusToolDef, agentStatusTool, agentCancelToolDef, agentCancelTool, agentSendInputToolDef, agentSendInputTool, } from "./agent.js";
|
|
19
|
-
import { enterPlanModeToolDef, enterPlanModeTool, exitPlanModeToolDef, exitPlanModeTool } from "./plan.js";
|
|
19
|
+
import { enterPlanModeToolDef, enterPlanModeTool, exitPlanModeToolDef, exitPlanModeTool, } from "./plan.js";
|
|
20
20
|
import { toolSearchToolDef, toolSearchTool } from "./tool-search.js";
|
|
21
21
|
import { todoWriteToolDef, todoWriteTool } from "./task.js";
|
|
22
|
-
import { enterWorktreeToolDef, enterWorktreeTool, exitWorktreeToolDef, exitWorktreeTool } from "./worktree.js";
|
|
22
|
+
import { enterWorktreeToolDef, enterWorktreeTool, exitWorktreeToolDef, exitWorktreeTool, switchSessionWorkspaceToolDef, switchSessionWorkspaceTool, } from "./worktree.js";
|
|
23
23
|
import { sleepToolDef, sleepTool } from "./sleep.js";
|
|
24
24
|
import { configToolDef, configTool } from "./config.js";
|
|
25
25
|
import { notebookEditToolDef, notebookEditTool } from "./notebook-edit.js";
|
|
26
26
|
import { lspToolDef, lspTool } from "./lsp.js";
|
|
27
|
-
import { cronCreateToolDef, cronCreateTool, cronDeleteToolDef, cronDeleteTool, cronListToolDef, cronListTool } from "./cron.js";
|
|
28
|
-
import { driveClaudeCodeToolDef, driveClaudeCodeTool, driveAgentToolDef, driveAgentTool } from "./drive-claude-code.js";
|
|
27
|
+
import { cronCreateToolDef, cronCreateTool, cronDeleteToolDef, cronDeleteTool, cronListToolDef, cronListTool, } from "./cron.js";
|
|
28
|
+
import { driveClaudeCodeToolDef, driveClaudeCodeTool, driveAgentToolDef, driveAgentTool, DRIVE_AGENT_TOOL_TIMEOUT_MS, } from "./drive-claude-code.js";
|
|
29
29
|
import { checkQuotaToolDef, checkQuotaTool } from "./check-quota.js";
|
|
30
30
|
import { skillToolDef, skillTool } from "./skill.js";
|
|
31
|
-
import { mcpToolDef, mcpToolExecute, listMcpResourcesToolDef, listMcpResourcesTool, readMcpResourceToolDef, readMcpResourceTool } from "./mcp-tools.js";
|
|
31
|
+
import { mcpToolDef, mcpToolExecute, listMcpResourcesToolDef, listMcpResourcesTool, readMcpResourceToolDef, readMcpResourceTool, } from "./mcp-tools.js";
|
|
32
32
|
import { replToolDef, replTool } from "./repl.js";
|
|
33
33
|
import { briefToolDef, briefTool } from "./brief.js";
|
|
34
34
|
import { powershellToolDef, powershellTool } from "./powershell.js";
|
|
@@ -39,9 +39,9 @@ import { cancelGoalToolDef, cancelGoalTool } from "./cancel-goal.js";
|
|
|
39
39
|
import { addMarketplaceToolDef, addMarketplaceTool } from "./add-marketplace.js";
|
|
40
40
|
import { bashOutputToolDef, bashOutputTool, killShellToolDef, killShellTool, listShellsToolDef, listShellsTool, } from "./background-shell-tools.js";
|
|
41
41
|
import { browserObserveToolDef, browserObserveTool, browserActToolDef, browserActTool, browserNavigateToolDef, browserNavigateTool, } from "./browser-tools.js";
|
|
42
|
-
import { useCredentialToolDef,
|
|
42
|
+
import { useCredentialToolDef, useCredentialBuiltinTool, } from "../../credentials/use-credential-tool.js";
|
|
43
43
|
import { injectCredentialToolDef, injectCredentialTool, isInjectCredentialAvailable, } from "../../credentials/inject-credential-tool.js";
|
|
44
|
-
import {
|
|
44
|
+
import { credentialAccessScope, getCredentialAccess } from "../../credentials/access.js";
|
|
45
45
|
export const BUILTIN_TOOLS = [
|
|
46
46
|
{
|
|
47
47
|
definition: {
|
|
@@ -69,7 +69,7 @@ export const BUILTIN_TOOLS = [
|
|
|
69
69
|
definition: {
|
|
70
70
|
...editModelCatalogToolDef,
|
|
71
71
|
source: "builtin",
|
|
72
|
-
permissionDefault: "ask", //
|
|
72
|
+
permissionDefault: "ask", // UI hint; default classifier fallback confirms writes.
|
|
73
73
|
isReadOnly: false,
|
|
74
74
|
isConcurrencySafe: false, // serializes writes to the single catalog file
|
|
75
75
|
},
|
|
@@ -344,6 +344,16 @@ export const BUILTIN_TOOLS = [
|
|
|
344
344
|
},
|
|
345
345
|
execute: exitWorktreeTool,
|
|
346
346
|
},
|
|
347
|
+
{
|
|
348
|
+
definition: {
|
|
349
|
+
...switchSessionWorkspaceToolDef,
|
|
350
|
+
source: "builtin",
|
|
351
|
+
permissionDefault: "ask",
|
|
352
|
+
isReadOnly: false,
|
|
353
|
+
isConcurrencySafe: false,
|
|
354
|
+
},
|
|
355
|
+
execute: switchSessionWorkspaceTool,
|
|
356
|
+
},
|
|
347
357
|
// ─── Phase 5: Utility Tools ────────────────────────────────────
|
|
348
358
|
{
|
|
349
359
|
definition: {
|
|
@@ -433,6 +443,8 @@ export const BUILTIN_TOOLS = [
|
|
|
433
443
|
permissionDefault: "ask",
|
|
434
444
|
isReadOnly: false,
|
|
435
445
|
isConcurrencySafe: false,
|
|
446
|
+
timeoutMs: DRIVE_AGENT_TOOL_TIMEOUT_MS,
|
|
447
|
+
pathPolicy: [{ kind: "arg", arg: "attachmentPaths", operation: "read" }],
|
|
436
448
|
},
|
|
437
449
|
execute: driveAgentTool,
|
|
438
450
|
},
|
|
@@ -445,6 +457,8 @@ export const BUILTIN_TOOLS = [
|
|
|
445
457
|
permissionDefault: "ask",
|
|
446
458
|
isReadOnly: false,
|
|
447
459
|
isConcurrencySafe: false,
|
|
460
|
+
timeoutMs: DRIVE_AGENT_TOOL_TIMEOUT_MS,
|
|
461
|
+
pathPolicy: [{ kind: "arg", arg: "attachmentPaths", operation: "read" }],
|
|
448
462
|
},
|
|
449
463
|
execute: driveClaudeCodeTool,
|
|
450
464
|
},
|
|
@@ -638,7 +652,7 @@ export const BUILTIN_TOOLS = [
|
|
|
638
652
|
},
|
|
639
653
|
// Browser automation — 3 semantic tools driving the in-app webview via the
|
|
640
654
|
// BrowserBridge (CDP). All serial on one webview (isConcurrencySafe:false).
|
|
641
|
-
// browser_observe is read-only. browser_act
|
|
655
|
+
// browser_observe is read-only. browser_act declares a UI hint of allow; its
|
|
642
656
|
// sensitive actions (click/type/select) are escalated to "ask" by a preset
|
|
643
657
|
// PermissionRule keyed on argsPattern { action } (see preset/index.ts §4.6) —
|
|
644
658
|
// so one tool carries per-action gating. Sensitive-action + domain-whitelist
|
|
@@ -659,7 +673,7 @@ export const BUILTIN_TOOLS = [
|
|
|
659
673
|
definition: {
|
|
660
674
|
...browserActToolDef,
|
|
661
675
|
source: "builtin",
|
|
662
|
-
permissionDefault: "allow", // per-action gating
|
|
676
|
+
permissionDefault: "allow", // UI hint; per-action execution gating is the preset rule.
|
|
663
677
|
isReadOnly: false,
|
|
664
678
|
isConcurrencySafe: false,
|
|
665
679
|
timeoutMs: 30_000, // the wait action internally bounds; give RPC headroom
|
|
@@ -677,8 +691,8 @@ export const BUILTIN_TOOLS = [
|
|
|
677
691
|
execute: browserNavigateTool,
|
|
678
692
|
},
|
|
679
693
|
// ─── Credentials: AI 取用已存凭证(token/link/cookie) ──────────
|
|
680
|
-
// permissionDefault:"allow"
|
|
681
|
-
// (默认问/本会话记住/全自动)
|
|
694
|
+
// permissionDefault:"allow" 是展示/声明 hint;取用审批由工具内部的
|
|
695
|
+
// CredentialUseGate 负责(默认问/本会话记住/全自动),避免双重弹窗。
|
|
682
696
|
// 读取凭证库本身不写文件(cookie 物化成临时 cookies.txt 是用完即弃的副产物)。
|
|
683
697
|
{
|
|
684
698
|
definition: {
|
|
@@ -688,10 +702,10 @@ export const BUILTIN_TOOLS = [
|
|
|
688
702
|
isReadOnly: true,
|
|
689
703
|
isConcurrencySafe: true,
|
|
690
704
|
},
|
|
691
|
-
execute:
|
|
705
|
+
execute: useCredentialBuiltinTool,
|
|
692
706
|
},
|
|
693
707
|
// InjectCredential:把 cookie 凭证注入内置浏览器(恢复登录态)。审批由工具内部
|
|
694
|
-
// CredentialUseGate 负责(逐条 autoInjectByAI)
|
|
708
|
+
// CredentialUseGate 负责(逐条 autoInjectByAI),permissionDefault:"allow" 仅作展示 hint。
|
|
695
709
|
// 改浏览器登录态有副作用 → 非 read-only。
|
|
696
710
|
{
|
|
697
711
|
definition: {
|
|
@@ -718,15 +732,17 @@ export const BUILTIN_TOOL_GUARDS = new Map([
|
|
|
718
732
|
// of the tool list (and the context) for the common no-credentials case,
|
|
719
733
|
// matching the spec's "quiet when empty" intent (true ToolSearch-deferral for
|
|
720
734
|
// builtins isn't wired in the engine).
|
|
721
|
-
[useCredentialToolDef.name, (ctx) => isUseCredentialAvailable(ctx.cwd)],
|
|
735
|
+
[useCredentialToolDef.name, (ctx) => isUseCredentialAvailable(ctx.cwd, ctx.settingsScope)],
|
|
722
736
|
// InjectCredential hidden until ≥1 cookie credential exists (browser injection
|
|
723
737
|
// is cookie-only). Also degrades at call time if no browser bridge is wired.
|
|
724
|
-
[injectCredentialToolDef.name, (ctx) => isInjectCredentialAvailable(ctx.cwd)],
|
|
738
|
+
[injectCredentialToolDef.name, (ctx) => isInjectCredentialAvailable(ctx.cwd, ctx.settingsScope)],
|
|
739
|
+
[completeGoalToolDef.name, (ctx) => ctx.hasGoal === true],
|
|
740
|
+
[cancelGoalToolDef.name, (ctx) => ctx.hasGoal === true],
|
|
725
741
|
]);
|
|
726
|
-
/** UseCredential is available when the cwd's
|
|
727
|
-
export function isUseCredentialAvailable(cwd) {
|
|
742
|
+
/** UseCredential is available when the cwd's credential metadata has ≥1 entry. */
|
|
743
|
+
export function isUseCredentialAvailable(cwd, settingsScope) {
|
|
728
744
|
try {
|
|
729
|
-
return
|
|
745
|
+
return getCredentialAccess().listMasked(cwd, credentialAccessScope(settingsScope)).length > 0;
|
|
730
746
|
}
|
|
731
747
|
catch {
|
|
732
748
|
return false;
|
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
* LSPTool — language server protocol operations for code intelligence.
|
|
3
3
|
*/
|
|
4
4
|
import type { ToolDefinition } from "../../types.js";
|
|
5
|
+
import type { ToolContext } from "../context.js";
|
|
5
6
|
export declare const lspToolDef: ToolDefinition;
|
|
6
|
-
export declare function lspTool(args: Record<string, unknown
|
|
7
|
+
export declare function lspTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { getLSPManager } from "../../lsp/manager.js";
|
|
5
5
|
import { detectLSPServer } from "../../lsp/servers.js";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { isAbsolute, resolve } from "node:path";
|
|
7
8
|
export const lspToolDef = {
|
|
8
9
|
name: "LSP",
|
|
9
10
|
description: "Use Language Server Protocol for code intelligence operations. " +
|
|
@@ -33,13 +34,15 @@ export const lspToolDef = {
|
|
|
33
34
|
required: ["action", "file_path"],
|
|
34
35
|
},
|
|
35
36
|
};
|
|
36
|
-
export async function lspTool(args) {
|
|
37
|
+
export async function lspTool(args, ctx) {
|
|
37
38
|
const action = args.action;
|
|
38
|
-
const
|
|
39
|
+
const rawPath = args.file_path;
|
|
39
40
|
const line = args.line ?? 0;
|
|
40
41
|
const character = args.character ?? 0;
|
|
41
|
-
if (!
|
|
42
|
+
if (!rawPath)
|
|
42
43
|
return "Error: file_path is required";
|
|
44
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
45
|
+
const filePath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
|
|
43
46
|
const manager = getLSPManager();
|
|
44
47
|
if (!manager)
|
|
45
48
|
return "Error: LSP is not initialized. Language servers are not available.";
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
import { MemoryManager } from "../../session/memory.js";
|
|
18
18
|
const VALID_SCOPES = ["user", "dream"];
|
|
19
19
|
const VALID_TYPES = ["user", "feedback", "project", "reference"];
|
|
20
|
+
const VALID_ORIGINS = ["auto", "manual", "dream"];
|
|
20
21
|
function parseScope(raw) {
|
|
21
22
|
if (typeof raw !== "string")
|
|
22
|
-
return
|
|
23
|
+
return 'Error: scope is required ("user" or "dream")';
|
|
23
24
|
if (!VALID_SCOPES.includes(raw)) {
|
|
24
25
|
return `Error: scope must be "user" or "dream", got "${raw}"`;
|
|
25
26
|
}
|
|
@@ -30,6 +31,11 @@ function parseScope(raw) {
|
|
|
30
31
|
function parseLocation(raw) {
|
|
31
32
|
return raw === "global" ? "global" : "project";
|
|
32
33
|
}
|
|
34
|
+
function parseOrigin(raw) {
|
|
35
|
+
return typeof raw === "string" && VALID_ORIGINS.includes(raw)
|
|
36
|
+
? raw
|
|
37
|
+
: undefined;
|
|
38
|
+
}
|
|
33
39
|
function mmFor(ctx, scope, location = "project") {
|
|
34
40
|
// location=global → omit projectDir so the manager points at the global store.
|
|
35
41
|
return new MemoryManager({
|
|
@@ -46,7 +52,7 @@ const LOCATION_SCHEMA = {
|
|
|
46
52
|
export const memoryListToolDef = {
|
|
47
53
|
name: "MemoryList",
|
|
48
54
|
description: "List persistent memory entries from one scope. " +
|
|
49
|
-
"Returns each entry's name, type, and short description (not the full content — use MemoryRead for that). " +
|
|
55
|
+
"Returns each entry's id, origin, counts, name, type, and short description (not the full content — use MemoryRead for that). " +
|
|
50
56
|
"Use this before MemorySave/MemoryDelete to find the exact name to target. " +
|
|
51
57
|
'Scopes: "user" (entries the user owns; you need permission to modify) or "dream" (auto-consolidation workspace; you may freely modify).',
|
|
52
58
|
inputSchema: {
|
|
@@ -73,7 +79,9 @@ export async function memoryListTool(args, ctx) {
|
|
|
73
79
|
if (entries.length === 0)
|
|
74
80
|
return `(no memories in scope "${scope}")`;
|
|
75
81
|
return entries
|
|
76
|
-
.map((e) => `- [${e.type}] ${e.name}
|
|
82
|
+
.map((e) => `- [${e.type}] ${e.name} ` +
|
|
83
|
+
`(id:${e.id ?? "(none)"}, origin:${e.origin ?? "manual"}, use:${e.useCount ?? 0}, updates:${e.updateCount ?? 0}) — ` +
|
|
84
|
+
e.description)
|
|
77
85
|
.join("\n");
|
|
78
86
|
}
|
|
79
87
|
catch (err) {
|
|
@@ -127,15 +135,24 @@ export async function memoryReadTool(args, ctx) {
|
|
|
127
135
|
if (cb) {
|
|
128
136
|
// scope here is always "user"|"dream" (validated above); pending is
|
|
129
137
|
// never tool-readable, so the narrower event type is correct.
|
|
130
|
-
void cb({
|
|
138
|
+
void cb({
|
|
139
|
+
type: "memory_recalled",
|
|
140
|
+
name: entry.name,
|
|
141
|
+
scope: scope,
|
|
142
|
+
location,
|
|
143
|
+
});
|
|
131
144
|
}
|
|
132
145
|
}
|
|
133
146
|
catch {
|
|
134
147
|
// ignore — the read result below is what matters
|
|
135
148
|
}
|
|
136
149
|
return (`name: ${entry.name}\n` +
|
|
150
|
+
`id: ${entry.id ?? ""}\n` +
|
|
137
151
|
`description: ${entry.description}\n` +
|
|
138
152
|
`type: ${entry.type}\n` +
|
|
153
|
+
`origin: ${entry.origin ?? "manual"}\n` +
|
|
154
|
+
`useCount: ${entry.useCount ?? 0}\n` +
|
|
155
|
+
`updateCount: ${entry.updateCount ?? 0}\n` +
|
|
139
156
|
`\n${entry.content}`);
|
|
140
157
|
}
|
|
141
158
|
catch (err) {
|
|
@@ -145,7 +162,7 @@ export async function memoryReadTool(args, ctx) {
|
|
|
145
162
|
// ─── MemorySave ────────────────────────────────────────────────────────────
|
|
146
163
|
export const memorySaveToolDef = {
|
|
147
164
|
name: "MemorySave",
|
|
148
|
-
description: "Create or
|
|
165
|
+
description: "Create or update a memory entry. Before saving, scan the injected memory index and/or call MemoryList; for the same durable topic, update the existing entry by id instead of creating date-stamped variants. " +
|
|
149
166
|
'Saving to scope "user" requires user permission (you will see a confirmation prompt). ' +
|
|
150
167
|
'Saving to scope "dream" is automatic — it is your auto-consolidation workspace. ' +
|
|
151
168
|
"Pick `type` carefully: " +
|
|
@@ -154,7 +171,7 @@ export const memorySaveToolDef = {
|
|
|
154
171
|
"project (non-obvious facts about ongoing work), " +
|
|
155
172
|
"reference (pointers to external resources). " +
|
|
156
173
|
"Pick `location`: global (a lesson/preference true in ANY project) vs project (this repo only). " +
|
|
157
|
-
"If a memory in the injected index is now stale or wrong,
|
|
174
|
+
"If a memory in the injected index is now stale or wrong, update it by id or MemoryDelete it — keep the store correct rather than letting contradictions pile up. " +
|
|
158
175
|
"The `description` is a one-line summary shown in the index; the `content` is the full body.",
|
|
159
176
|
inputSchema: {
|
|
160
177
|
type: "object",
|
|
@@ -165,9 +182,13 @@ export const memorySaveToolDef = {
|
|
|
165
182
|
description: "Which scope to save to",
|
|
166
183
|
},
|
|
167
184
|
location: LOCATION_SCHEMA,
|
|
185
|
+
id: {
|
|
186
|
+
type: "string",
|
|
187
|
+
description: "Stable id from MemoryList/MemoryRead when updating an existing memory. Omit only for genuinely new topics.",
|
|
188
|
+
},
|
|
168
189
|
name: {
|
|
169
190
|
type: "string",
|
|
170
|
-
description: "Short
|
|
191
|
+
description: "Short stable topic identifier; avoid dates, versions, or batch suffixes",
|
|
171
192
|
},
|
|
172
193
|
description: {
|
|
173
194
|
type: "string",
|
|
@@ -192,11 +213,14 @@ export async function memorySaveTool(args, ctx) {
|
|
|
192
213
|
return scope;
|
|
193
214
|
}
|
|
194
215
|
const name = args.name;
|
|
216
|
+
const id = args.id;
|
|
195
217
|
const description = args.description;
|
|
196
218
|
const type = args.type;
|
|
197
219
|
const content = args.content;
|
|
198
220
|
if (typeof name !== "string" || !name)
|
|
199
221
|
return "Error: name is required";
|
|
222
|
+
if (id !== undefined && typeof id !== "string")
|
|
223
|
+
return "Error: id must be a string";
|
|
200
224
|
if (typeof description !== "string")
|
|
201
225
|
return "Error: description is required";
|
|
202
226
|
if (typeof content !== "string")
|
|
@@ -207,12 +231,20 @@ export async function memorySaveTool(args, ctx) {
|
|
|
207
231
|
try {
|
|
208
232
|
const location = parseLocation(args.location);
|
|
209
233
|
const mm = mmFor(ctx, scope, location);
|
|
234
|
+
const dreamLoop = ctx?.__dreamLoop === true;
|
|
235
|
+
const forcedOrigin = dreamLoop
|
|
236
|
+
? "dream"
|
|
237
|
+
: scope === "user"
|
|
238
|
+
? "manual"
|
|
239
|
+
: (parseOrigin(args.origin) ?? "manual");
|
|
210
240
|
const fileName = mm.save({
|
|
241
|
+
id,
|
|
211
242
|
name,
|
|
212
243
|
description,
|
|
213
244
|
type: type,
|
|
214
245
|
content,
|
|
215
|
-
|
|
246
|
+
origin: forcedOrigin,
|
|
247
|
+
}, { forceOrigin: forcedOrigin });
|
|
216
248
|
return `Saved memory "${name}" → ${location}/${scope}/${fileName}`;
|
|
217
249
|
}
|
|
218
250
|
catch (err) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* NotebookEditTool — read and edit Jupyter notebook (.ipynb) cells.
|
|
3
3
|
*/
|
|
4
4
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
5
|
+
import { isAbsolute, resolve } from "node:path";
|
|
5
6
|
export const notebookEditToolDef = {
|
|
6
7
|
name: "NotebookEdit",
|
|
7
8
|
description: "Edit a Jupyter notebook (.ipynb) file. Supports actions: " +
|
|
@@ -37,10 +38,12 @@ export const notebookEditToolDef = {
|
|
|
37
38
|
},
|
|
38
39
|
};
|
|
39
40
|
export async function notebookEditTool(args, ctx) {
|
|
40
|
-
const
|
|
41
|
+
const rawPath = args.file_path;
|
|
41
42
|
const action = args.action;
|
|
42
|
-
if (!
|
|
43
|
+
if (!rawPath)
|
|
43
44
|
return "Error: file_path is required";
|
|
45
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
46
|
+
const filePath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
|
|
44
47
|
if (!filePath.endsWith(".ipynb"))
|
|
45
48
|
return "Error: file must be a .ipynb file";
|
|
46
49
|
if (action === "read") {
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
* centralized in {@link safeSpawn}; this file only carries the pwsh /
|
|
6
6
|
* powershell.exe selection + the PowerShell-specific output formatting.
|
|
7
7
|
*/
|
|
8
|
-
import type { ToolDefinition } from "../../types.js";
|
|
8
|
+
import type { ToolDefinition, ToolResult } from "../../types.js";
|
|
9
9
|
import type { ToolContext } from "../context.js";
|
|
10
10
|
export declare const powershellToolDef: ToolDefinition;
|
|
11
|
-
export declare function powershellTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string
|
|
11
|
+
export declare function powershellTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string | {
|
|
12
|
+
result: string;
|
|
13
|
+
sandbox: ToolResult["sandbox"];
|
|
14
|
+
}>;
|
|
@@ -28,6 +28,10 @@ export const powershellToolDef = {
|
|
|
28
28
|
},
|
|
29
29
|
};
|
|
30
30
|
const PS_MAX_BUFFER = 5 * 1024 * 1024;
|
|
31
|
+
const POWERSHELL_SANDBOX_MARK = { backend: "off" };
|
|
32
|
+
function markPowerShellResult(result) {
|
|
33
|
+
return { result, sandbox: POWERSHELL_SANDBOX_MARK };
|
|
34
|
+
}
|
|
31
35
|
export async function powershellTool(args, ctx) {
|
|
32
36
|
const command = args.command;
|
|
33
37
|
// `??` only catches null/undefined — a non-positive timeout (0/-5) would slip
|
|
@@ -35,7 +39,7 @@ export async function powershellTool(args, ctx) {
|
|
|
35
39
|
const rawTimeout = args.timeout;
|
|
36
40
|
const timeout = typeof rawTimeout === "number" && rawTimeout > 0 ? rawTimeout : 120_000;
|
|
37
41
|
if (!command.trim()) {
|
|
38
|
-
return "Error: command is required.";
|
|
42
|
+
return markPowerShellResult("Error: command is required.");
|
|
39
43
|
}
|
|
40
44
|
// Determine PowerShell executable.
|
|
41
45
|
const psCmd = process.platform === "win32" ? "powershell.exe" : "pwsh";
|
|
@@ -52,19 +56,19 @@ export async function powershellTool(args, ctx) {
|
|
|
52
56
|
signal: ctx?.signal,
|
|
53
57
|
});
|
|
54
58
|
if (result.aborted) {
|
|
55
|
-
return wasAbortedBeforeStart
|
|
59
|
+
return markPowerShellResult(wasAbortedBeforeStart
|
|
56
60
|
? "PowerShell aborted before starting."
|
|
57
|
-
: "PowerShell aborted by signal.";
|
|
61
|
+
: "PowerShell aborted by signal.");
|
|
58
62
|
}
|
|
59
63
|
if (result.timedOut) {
|
|
60
|
-
return `PowerShell timed out after ${timeout}ms
|
|
64
|
+
return markPowerShellResult(`PowerShell timed out after ${timeout}ms`);
|
|
61
65
|
}
|
|
62
66
|
if (result.spawnFailed) {
|
|
63
|
-
return `PowerShell spawn error: ${result.error ?? "unknown error"}
|
|
67
|
+
return markPowerShellResult(`PowerShell spawn error: ${result.error ?? "unknown error"}`);
|
|
64
68
|
}
|
|
65
69
|
if (result.exitCode !== 0) {
|
|
66
70
|
const out = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
|
|
67
|
-
return `PowerShell error:\n${out || `exit code ${result.exitCode}`}
|
|
71
|
+
return markPowerShellResult(`PowerShell error:\n${out || `exit code ${result.exitCode}`}`);
|
|
68
72
|
}
|
|
69
|
-
return result.stdout.trim() || "(no output)";
|
|
73
|
+
return markPowerShellResult(result.stdout.trim() || "(no output)");
|
|
70
74
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in Read file tool.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { readFile, stat, open } from "node:fs/promises";
|
|
6
|
+
import { createReadStream, existsSync } from "node:fs";
|
|
7
|
+
import { extname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
6
8
|
import { fileCache } from "./file-cache.js";
|
|
7
9
|
import { toLf } from "./eol.js";
|
|
8
10
|
export const readToolDef = {
|
|
@@ -10,7 +12,8 @@ export const readToolDef = {
|
|
|
10
12
|
description: "Read a file from the local filesystem. Returns the file content with line numbers. " +
|
|
11
13
|
"By default reads up to 2000 lines from the beginning. " +
|
|
12
14
|
"Use offset and limit to read specific portions of large files. " +
|
|
13
|
-
"For
|
|
15
|
+
"For images and binary files, returns path/metadata instead of raw bytes. " +
|
|
16
|
+
"For large text files, consider using Grep first to find the relevant lines.\n\n" +
|
|
14
17
|
"Do NOT re-read a file you just edited to verify — Edit/Write would have errored " +
|
|
15
18
|
"if the change failed.\n" +
|
|
16
19
|
"Do NOT re-read a file (or the same range of a file) you've already read earlier in " +
|
|
@@ -27,18 +30,42 @@ export const readToolDef = {
|
|
|
27
30
|
},
|
|
28
31
|
};
|
|
29
32
|
const MAX_CONTENT_CHARS = 200_000;
|
|
33
|
+
const LARGE_TEXT_BYTES = 5 * 1024 * 1024;
|
|
34
|
+
const SAMPLE_BYTES = 8192;
|
|
35
|
+
const IMAGE_EXT_MIME = {
|
|
36
|
+
".png": "image/png",
|
|
37
|
+
".jpg": "image/jpeg",
|
|
38
|
+
".jpeg": "image/jpeg",
|
|
39
|
+
".gif": "image/gif",
|
|
40
|
+
".webp": "image/webp",
|
|
41
|
+
};
|
|
30
42
|
export async function readTool(args, ctx) {
|
|
31
|
-
const
|
|
32
|
-
if (!
|
|
43
|
+
const rawPath = args.file_path;
|
|
44
|
+
if (!rawPath)
|
|
33
45
|
return "Error: file_path is required";
|
|
46
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
47
|
+
const filePath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
|
|
34
48
|
if (!existsSync(filePath))
|
|
35
49
|
return `Error: File not found: ${filePath}`;
|
|
36
50
|
try {
|
|
37
51
|
// Get file info first
|
|
38
52
|
const fileInfo = await stat(filePath);
|
|
39
53
|
const sizeKB = Math.round(fileInfo.size / 1024);
|
|
54
|
+
const sample = await readSample(filePath, Math.min(fileInfo.size, SAMPLE_BYTES));
|
|
55
|
+
const binary = detectBinary(filePath, sample);
|
|
56
|
+
if (binary) {
|
|
57
|
+
const sha256 = await sha256File(filePath);
|
|
58
|
+
return formatBinaryReadResult({
|
|
59
|
+
filePath,
|
|
60
|
+
cwd,
|
|
61
|
+
size: fileInfo.size,
|
|
62
|
+
sha256,
|
|
63
|
+
mime: binary.mime,
|
|
64
|
+
isImage: binary.kind === "image",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
40
67
|
// Skip binary or excessively large files
|
|
41
|
-
if (fileInfo.size >
|
|
68
|
+
if (fileInfo.size > LARGE_TEXT_BYTES) {
|
|
42
69
|
return `Error: File is too large (${sizeKB}KB). Use Grep to search for specific content, or provide offset and limit to read a portion.`;
|
|
43
70
|
}
|
|
44
71
|
// Try cache first, fall back to disk read
|
|
@@ -76,3 +103,88 @@ export async function readTool(args, ctx) {
|
|
|
76
103
|
return `Error reading file: ${err.message}`;
|
|
77
104
|
}
|
|
78
105
|
}
|
|
106
|
+
async function readSample(filePath, length) {
|
|
107
|
+
if (length <= 0)
|
|
108
|
+
return Buffer.alloc(0);
|
|
109
|
+
const handle = await open(filePath, "r");
|
|
110
|
+
try {
|
|
111
|
+
const buffer = Buffer.alloc(length);
|
|
112
|
+
const { bytesRead } = await handle.read(buffer, 0, length, 0);
|
|
113
|
+
return buffer.subarray(0, bytesRead);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
await handle.close();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function detectBinary(filePath, sample) {
|
|
120
|
+
const imageMime = detectImageMime(filePath, sample);
|
|
121
|
+
if (imageMime)
|
|
122
|
+
return { kind: "image", mime: imageMime };
|
|
123
|
+
if (sample.length === 0)
|
|
124
|
+
return null;
|
|
125
|
+
if (sample.includes(0))
|
|
126
|
+
return { kind: "binary" };
|
|
127
|
+
try {
|
|
128
|
+
new TextDecoder("utf-8", { fatal: true }).decode(sample);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return { kind: "binary" };
|
|
132
|
+
}
|
|
133
|
+
let controls = 0;
|
|
134
|
+
for (const byte of sample) {
|
|
135
|
+
if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 12 && byte !== 13)
|
|
136
|
+
controls += 1;
|
|
137
|
+
}
|
|
138
|
+
return controls > Math.max(8, sample.length * 0.01) ? { kind: "binary" } : null;
|
|
139
|
+
}
|
|
140
|
+
function detectImageMime(filePath, sample) {
|
|
141
|
+
if (sample.length >= 8 &&
|
|
142
|
+
sample[0] === 0x89 &&
|
|
143
|
+
sample[1] === 0x50 &&
|
|
144
|
+
sample[2] === 0x4e &&
|
|
145
|
+
sample[3] === 0x47 &&
|
|
146
|
+
sample[4] === 0x0d &&
|
|
147
|
+
sample[5] === 0x0a &&
|
|
148
|
+
sample[6] === 0x1a &&
|
|
149
|
+
sample[7] === 0x0a) {
|
|
150
|
+
return "image/png";
|
|
151
|
+
}
|
|
152
|
+
if (sample.length >= 3 && sample[0] === 0xff && sample[1] === 0xd8 && sample[2] === 0xff) {
|
|
153
|
+
return "image/jpeg";
|
|
154
|
+
}
|
|
155
|
+
const header = sample.subarray(0, 12).toString("ascii");
|
|
156
|
+
if (header.startsWith("GIF87a") || header.startsWith("GIF89a"))
|
|
157
|
+
return "image/gif";
|
|
158
|
+
if (header.startsWith("RIFF") && header.slice(8, 12) === "WEBP")
|
|
159
|
+
return "image/webp";
|
|
160
|
+
return IMAGE_EXT_MIME[extname(filePath).toLowerCase()];
|
|
161
|
+
}
|
|
162
|
+
async function sha256File(filePath) {
|
|
163
|
+
const hash = createHash("sha256");
|
|
164
|
+
for await (const chunk of createReadStream(filePath)) {
|
|
165
|
+
hash.update(chunk);
|
|
166
|
+
}
|
|
167
|
+
return hash.digest("hex");
|
|
168
|
+
}
|
|
169
|
+
function formatBinaryReadResult(input) {
|
|
170
|
+
const path = displayPath(input.filePath, input.cwd);
|
|
171
|
+
const lines = [
|
|
172
|
+
input.isImage ? "Image file (not displayed by Read)." : "Binary file (not displayed by Read).",
|
|
173
|
+
`Path: ${path}`,
|
|
174
|
+
`Absolute path: ${input.filePath}`,
|
|
175
|
+
...(input.mime ? [`MIME: ${input.mime}`] : []),
|
|
176
|
+
`Size: ${input.size} bytes`,
|
|
177
|
+
`SHA-256: ${input.sha256}`,
|
|
178
|
+
];
|
|
179
|
+
if (input.isImage) {
|
|
180
|
+
lines.push(`Use view_image({ path: "${path}" }) to inspect pixels.`);
|
|
181
|
+
}
|
|
182
|
+
return lines.join("\n");
|
|
183
|
+
}
|
|
184
|
+
function displayPath(filePath, cwd) {
|
|
185
|
+
const rel = relative(cwd, filePath);
|
|
186
|
+
if (rel && rel !== ".." && !rel.startsWith(`..${sep}`)) {
|
|
187
|
+
return sep === "\\" ? rel.replace(/\\/g, "/") : rel;
|
|
188
|
+
}
|
|
189
|
+
return filePath;
|
|
190
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-in view_image tool —
|
|
3
|
-
* 回传进上下文,让 vision 模型「看」它(对照 codex 的 view_image)。
|
|
2
|
+
* Built-in view_image tool — 把一个本地图片文件或历史图片以 base64 image
|
|
3
|
+
* ContentBlock 回传进上下文,让 vision 模型「看」它(对照 codex 的 view_image)。
|
|
4
4
|
*
|
|
5
5
|
* 典型用法:模型先写 SVG/Mermaid 并用 shell 转成 PNG,再调 view_image(png)
|
|
6
6
|
* 检查图画对没有(标签是否重叠、文字是否溢出),不对就改源再重转。
|