@tencent-ai/agent-sdk 0.3.235 → 0.3.236
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/cli/CHANGELOG.md +23 -0
- package/cli/dist/codebuddy-headless.js +183 -160
- package/cli/package.json +1 -1
- package/cli/product.cloudhosted.json +2 -2
- package/cli/product.internal.json +2 -2
- package/cli/product.ioa.json +2 -2
- package/cli/product.json +5 -5
- package/cli/product.selfhosted.json +2 -2
- package/cli/vendor/shim/node-safe-delete-shim.cjs +10 -1
- package/cli/vendor/shim/safe-bin/safe-delete-common.sh +91 -34
- package/cli/vendor/shim/sitecustomize.py +12 -0
- package/package.json +1 -1
package/cli/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tencent-ai/codebuddy-code",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.131.0",
|
|
4
4
|
"description": "Use CodeBuddy, Tencent's AI assistant, right from your terminal. CodeBuddy can understand your codebase, edit files, run terminal commands, and handle entire workflows for you.",
|
|
5
5
|
"main": "lib/node/index.js",
|
|
6
6
|
"typings": "lib/node/index.d.ts",
|
|
@@ -835,6 +835,6 @@
|
|
|
835
835
|
"SelectImage": true,
|
|
836
836
|
"SkipToolCallSupportCheck": true
|
|
837
837
|
},
|
|
838
|
-
"commit": "
|
|
839
|
-
"date": "2026-07-
|
|
838
|
+
"commit": "6df0cc9cfa93d76207e707adf47091324daffb98",
|
|
839
|
+
"date": "2026-07-31T12:02:07.391Z"
|
|
840
840
|
}
|
package/cli/product.ioa.json
CHANGED
package/cli/product.json
CHANGED
|
@@ -735,7 +735,7 @@
|
|
|
735
735
|
},
|
|
736
736
|
{
|
|
737
737
|
"name": "tool-websearch-description",
|
|
738
|
-
"template": "\n- Searches the web and uses the results to inform responses\n- Provides up-to-date information for current events and recent data\n- Returns search result information formatted as search result blocks\n- Use this tool for accessing information beyond the knowledge cutoff\n- Searches are performed automatically within a single API call\n- You MUST use the
|
|
738
|
+
"template": "\n- Searches the web and uses the results to inform responses\n- Provides up-to-date information for current events and recent data\n- Returns search result information formatted as search result blocks\n- Use this tool for accessing information beyond the knowledge cutoff\n- Searches are performed automatically within a single API call\n- You MUST use the current time provided in context when searching for recent information.\n\nWhen to search:\n - For any factual question about the present-day world, you MUST search before answering; search before EVERY such question.\n - Your confidence on a topic is NOT an excuse to skip search. Present-day facts — who holds a role, what something costs, whether a law still applies, what's newest in a category — cannot come from training data.\n - Even seemingly familiar questions — for example asking about the price of a product or who currently leads a country — can be outdated, because prices and officeholders change frequently.\n - Proactively search instead of answering from your priors and offering to check.\n\nUsage notes:\n - The query must preserve the meaning of the user's request; keep the entity names, versions, dates and terms the user mentioned, and do not substitute or fill them in from prior knowledge.\n - Domain filtering is supported to include or block specific websites.\n"
|
|
739
739
|
},
|
|
740
740
|
{
|
|
741
741
|
"name": "tool-enterplanmode-description",
|
|
@@ -891,7 +891,7 @@
|
|
|
891
891
|
},
|
|
892
892
|
{
|
|
893
893
|
"name": "tool-enterworktree-description",
|
|
894
|
-
"template": "Use this tool ONLY when the user explicitly asks to work in a worktree.\nThis tool creates an isolated git worktree and switches the current session into it.\n\n## When to Use\n\n- The user explicitly says \"worktree\"\n (e.g., \"start a worktree\", \"work in a worktree\", \"create a worktree\", \"use a worktree\")\n\n## When NOT to Use\n\n- The user asks to create a branch, switch branches, or work on a different branch — use git commands instead\n- The user asks to fix a bug or work on a feature — use normal git workflow unless they specifically mention worktrees\n- Never use this tool unless the user explicitly mentions \"worktree\"\n\n## Requirements\n\n- Must be in a git repository, OR have WorktreeCreate/WorktreeRemove hooks configured in settings.json\n- Must not already be in a worktree\n\n## Behavior\n\n- In a git repository: creates a new git worktree inside `.codebuddy/worktrees/` with a new branch based on HEAD\n- Outside a git repository: delegates to WorktreeCreate/WorktreeRemove hooks for VCS-agnostic isolation\n- Switches the session's working directory to the new worktree\n- On session exit, the user will be prompted to keep or remove the worktree\n\n## Parameters\n\n- `name` (optional): A name for the worktree. If not provided, a random name is generated.\n"
|
|
894
|
+
"template": "Use this tool ONLY when the user explicitly asks to work in a worktree.\nThis tool creates an isolated git worktree and switches the current session into it.\n\n## When to Use\n\n- The user explicitly says \"worktree\"\n (e.g., \"start a worktree\", \"work in a worktree\", \"create a worktree\", \"use a worktree\")\n\n## When NOT to Use\n\n- The user asks to create a branch, switch branches, or work on a different branch — use git commands instead\n- The user asks to fix a bug or work on a feature — use normal git workflow unless they specifically mention worktrees\n- Never use this tool unless the user explicitly mentions \"worktree\"\n\n## Requirements\n\n- Must be in a git repository, OR have WorktreeCreate/WorktreeRemove hooks configured in settings.json\n- Must not already be in a worktree\n\n## Behavior\n\n- In a git repository: creates a new git worktree inside `.codebuddy/worktrees/` with a new branch based on HEAD\n- Outside a git repository: delegates to WorktreeCreate/WorktreeRemove hooks for VCS-agnostic isolation\n- Switches the session's working directory to the new worktree\n- On session exit, the user will be prompted to keep or remove the worktree\n- When `path` is provided: attaches to (switches into) that existing worktree instead of creating a new one. The pre-existing worktree is never removed on exit (always kept), since it was not created by this session.\n\n## Parameters\n\n- `name` (optional): A name for the worktree. If not provided, a random name is generated. Ignored when `path` is provided.\n- `branch` (optional): The base branch to create the worktree from. Ignored when `path` is provided.\n- `path` (optional): Path to an existing git worktree to attach to instead of creating a new one. Must be a linked worktree of the current repository (not the main working tree); otherwise the tool returns an error.\n\n"
|
|
895
895
|
},
|
|
896
896
|
{
|
|
897
897
|
"name": "tool-croncreate-description",
|
|
@@ -911,7 +911,7 @@
|
|
|
911
911
|
},
|
|
912
912
|
{
|
|
913
913
|
"name": "skill-loop-prompt",
|
|
914
|
-
"template": "# /loop — schedule a recurring prompt\n\nParse the input below into `[interval] <prompt…>` and schedule it with CronCreate.\n\n## Parsing (in priority order)\n\n1. **Leading token**: if the first whitespace-delimited token matches `^\\d+[smhd]$` (e.g. `5m`, `2h`), that's the interval; the rest is the prompt.\n2. **Trailing \"every\" clause**: otherwise, if the input ends with `every <N><unit>` or `every <N> <unit-word>` (e.g. `every 20m`, `every 5 minutes`, `every 2 hours`), extract that as the interval and strip it from the prompt. Only match when what follows \"every\" is a time expression — `check every PR` has no interval.\n3. **Default**: otherwise, interval is `10m` and the entire input is the prompt.\n\nIf the resulting prompt is empty, show usage `/loop [interval] <prompt>` and stop — do not call CronCreate.\n\nExamples:\n- `5m /babysit-prs` → interval `5m`, prompt `/babysit-prs` (rule 1)\n- `check the deploy every 20m` → interval `20m`, prompt `check the deploy` (rule 2)\n- `run tests every 5 minutes` → interval `5m`, prompt `run tests` (rule 2)\n- `check the deploy` → interval `10m`, prompt `check the deploy` (rule 3)\n- `check every PR` → interval `10m`, prompt `check every PR` (rule 3 — \"every\" not followed by time)\n- `5m` → empty prompt → show usage\n\n## Interval → cron\n\nSupported suffixes: `s` (seconds, rounded up to nearest minute, min 1), `m` (minutes), `h` (hours), `d` (days). Convert:\n\n| Interval pattern | Cron expression | Notes |\n|-----------------------|---------------------|------------------------------------------|\n| `Nm` where N ≤ 59 | `*/N * * * *` | every N minutes |\n| `Nm` where N ≥ 60 | `0 */H * * *` | round to hours (H = N/60, must divide 24)|\n| `Nh` where N ≤ 23 | `0 */N * * *` | every N hours |\n| `Nd` | `0 0 */N * *` | every N days at midnight local |\n| `Ns` | treat as `ceil(N/60)m` | cron minimum granularity is 1 minute |\n\n**If the interval doesn't cleanly divide its unit** (e.g. `7m` → `*/7 * * * *` gives uneven gaps at :56→:00; `90m` → 1.5h which cron can't express), pick the nearest clean interval and tell the user what you rounded to before scheduling.\n\n## Action\n\nCall CronCreate with:\n- `cron`: the expression from the table above\n- `prompt`: the
|
|
914
|
+
"template": "# /loop — schedule a recurring prompt\n\nParse the input below into `[interval] <prompt…>` and schedule it with CronCreate.\n\n## Parsing (in priority order)\n\n1. **Leading token**: if the first whitespace-delimited token matches `^\\d+[smhd]$` (e.g. `5m`, `2h`), that's the interval; the rest is the prompt.\n2. **Trailing \"every\" clause**: otherwise, if the input ends with `every <N><unit>` or `every <N> <unit-word>` (e.g. `every 20m`, `every 5 minutes`, `every 2 hours`), extract that as the interval and strip it from the prompt. Only match when what follows \"every\" is a time expression — `check every PR` has no interval.\n3. **Default**: otherwise, interval is `10m` and the entire input is the prompt.\n\nIf the resulting prompt is empty, show usage `/loop [interval] <prompt>` and stop — do not call CronCreate.\n\nExamples:\n- `5m /babysit-prs` → interval `5m`, prompt `/babysit-prs` (rule 1)\n- `check the deploy every 20m` → interval `20m`, prompt `check the deploy` (rule 2)\n- `run tests every 5 minutes` → interval `5m`, prompt `run tests` (rule 2)\n- `check the deploy` → interval `10m`, prompt `check the deploy` (rule 3)\n- `check every PR` → interval `10m`, prompt `check every PR` (rule 3 — \"every\" not followed by time)\n- `5m` → empty prompt → show usage\n\n## Interval → cron\n\nSupported suffixes: `s` (seconds, rounded up to nearest minute, min 1), `m` (minutes), `h` (hours), `d` (days). Convert:\n\n| Interval pattern | Cron expression | Notes |\n|-----------------------|---------------------|------------------------------------------|\n| `Nm` where N ≤ 59 | `*/N * * * *` | every N minutes |\n| `Nm` where N ≥ 60 | `0 */H * * *` | round to hours (H = N/60, must divide 24)|\n| `Nh` where N ≤ 23 | `0 */N * * *` | every N hours |\n| `Nd` | `0 0 */N * *` | every N days at midnight local |\n| `Ns` | treat as `ceil(N/60)m` | cron minimum granularity is 1 minute |\n\n**If the interval doesn't cleanly divide its unit** (e.g. `7m` → `*/7 * * * *` gives uneven gaps at :56→:00; `90m` → 1.5h which cron can't express), pick the nearest clean interval and tell the user what you rounded to before scheduling.\n\n## Action\n\nFirst pick a `<LOOP_ID>`: a short unique token you make up right now (e.g. 6–8 random lowercase-alphanumeric characters like `k3f9x2`). **Do not call any tool (e.g. Bash `date`) to build it** — just invent it inline. It only has to be unique per loop and stay identical across every reschedule of this loop. Also note today's date from the `<env>` block above as `<STARTED_DATE>` (a plain date is enough for the safety cap; you do not need a precise time).\n\nBuild the scheduled prompt by wrapping the parsed prompt with a self-adjustment block. Substitute `<LOOP_ID>` and `<STARTED_DATE>` with the values you just chose (set once here; never change them on later reschedules) and `<PROMPT>` with the parsed prompt. The `[/loop:<LOOP_ID>]` marker must stay at the very front so it survives CronList's truncated preview:\n\n```\n[/loop:<LOOP_ID>] <PROMPT>\n\n---\n[/loop self-adjustment] This prompt is fired by a /loop job (marker `[/loop:<LOOP_ID>]`, started <STARTED_DATE>). **Every single time this prompt fires**, after finishing the task above you must end the turn with one of:\n- **Continue looping** (default — same interval as before, or a new one you decide is better):\n 1. Find this job's id: check your conversation history for the CronCreate/CronDelete result that scheduled this exact firing; if not visible — e.g. after context compaction — call CronList and match the `[/loop:<LOOP_ID>]` marker at the start of a job's prompt preview.\n 2. If CronList matches more than one job with this exact marker (can happen if a previous CronDelete failed), delete all of them before creating the replacement — never leave duplicates running.\n 3. If CronDelete errors (e.g. job already gone), do not skip it silently — still proceed to CronCreate the replacement so the loop doesn't die.\n 4. Call CronCreate with `recurring: false` and this exact wrapped prompt (marker + self-adjustment block unchanged) at whatever time you decide the next iteration should run.\n- **Stop the loop**: call CronDelete with the job id (and any duplicates found via CronList) and do not create a replacement. Tell the user the loop stopped and why.\n\nDo not assume the job re-fires on its own — once this switches to a `recurring: false` job, nothing fires automatically; you must explicitly recreate it every time or the loop silently ends.\n\n**Safety cap**: if today's date is more than 3 days after <STARTED_DATE>, stop instead of continuing — this mirrors the built-in recurring task expiry, which only auto-enforces on the initial `recurring: true` job below, not on the one-shot jobs you create afterward.\n```\n\nCall CronCreate with:\n- `cron`: the expression from the table above\n- `prompt`: the wrapped prompt built above (not the bare parsed prompt)\n- `recurring`: `true`\n\nThen confirm to the user: what's scheduled, the cron expression, the human-readable cadence, that after the first firing it switches to self-managed rescheduling each round (so it keeps running only as long as it keeps recreating itself, and can stop itself early), that it's capped at 3 days either way, and that they can cancel sooner with CronDelete (include the job ID).\n\n## Input\n\n$ARGUMENTS\n"
|
|
915
915
|
},
|
|
916
916
|
{
|
|
917
917
|
"name": "workflow-tool-description",
|
|
@@ -1803,6 +1803,6 @@
|
|
|
1803
1803
|
"deferLoading": true
|
|
1804
1804
|
}
|
|
1805
1805
|
],
|
|
1806
|
-
"commit": "
|
|
1807
|
-
"date": "2026-07-
|
|
1806
|
+
"commit": "6df0cc9cfa93d76207e707adf47091324daffb98",
|
|
1807
|
+
"date": "2026-07-31T12:02:07.371Z"
|
|
1808
1808
|
}
|
|
@@ -348,6 +348,6 @@
|
|
|
348
348
|
"ScheduledTasks": true,
|
|
349
349
|
"SkipToolCallSupportCheck": true
|
|
350
350
|
},
|
|
351
|
-
"commit": "
|
|
352
|
-
"date": "2026-07-
|
|
351
|
+
"commit": "6df0cc9cfa93d76207e707adf47091324daffb98",
|
|
352
|
+
"date": "2026-07-31T12:02:07.366Z"
|
|
353
353
|
}
|
|
@@ -240,6 +240,7 @@ let _trashBinAvailable = null; // null=未探测, true=可用, false=不可用
|
|
|
240
240
|
|
|
241
241
|
function trashViaBinary(absPath) {
|
|
242
242
|
if (_trashBinAvailable === false) return false; // 已知不可用,快速短路
|
|
243
|
+
const existed = pathStillExists(absPath);
|
|
243
244
|
try {
|
|
244
245
|
execFileSync(resolveTrashBin(), [absPath], {
|
|
245
246
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -257,6 +258,14 @@ function trashViaBinary(absPath) {
|
|
|
257
258
|
if (e.code === 'EACCES') {
|
|
258
259
|
return false;
|
|
259
260
|
}
|
|
261
|
+
// Windows 上 trash crate 的 IFileOperation::GetAnyOperationsAborted 会误报 true,
|
|
262
|
+
// 但 PerformOperations 实际已删除文件。以文件系统事实为准:调用前在、调用后没了 = 删除成功。
|
|
263
|
+
// 仅 Windows 启用:根因是 Win32 独有 API,非 Windows 上失败原因未知,不应静默吞掉。
|
|
264
|
+
// 参考 https://github.com/maxkagamine/wsl-tools 的 recycle_bin.rs
|
|
265
|
+
if (PLATFORM === 'win32' && existed && !pathStillExists(absPath)) {
|
|
266
|
+
_trashBinAvailable = true;
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
260
269
|
// binary 已启动但对该路径操作失败(exit code 1)或超时——向上抛,不真删
|
|
261
270
|
const detail = e.stderr ? e.stderr.toString().trim() : '';
|
|
262
271
|
throw new Error(`[safe-delete] 操作失败: ${detail || e.message}`);
|
|
@@ -266,7 +275,7 @@ function trashViaBinary(absPath) {
|
|
|
266
275
|
/**
|
|
267
276
|
* 把文件/目录移入系统回收站。
|
|
268
277
|
* 优先 native binary(性能最优),不可用时自动降级到平台实现。
|
|
269
|
-
*
|
|
278
|
+
* 同步调用,失败抛错(fail-closed,调用方不得真删)。
|
|
270
279
|
*
|
|
271
280
|
* @param {string} absPath 绝对路径
|
|
272
281
|
*/
|
|
@@ -24,14 +24,11 @@ _safe_delete_diag() {
|
|
|
24
24
|
|
|
25
25
|
safe_delete_json_string() {
|
|
26
26
|
local value="$1"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
fi
|
|
33
|
-
local escaped
|
|
34
|
-
escaped="$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
|
27
|
+
# 纯 bash JSON 字符串转义:避免 python(Windows Store stub 每次调用 1-4 秒且 stdout 可能空,
|
|
28
|
+
# 写出非法 JSON 导致 readSafeDeleteReport 静默丢弃"已移入回收站"提示)和 sed(fork 开销)。
|
|
29
|
+
# 只转义 JSON 必需字符:\ 和 "。路径里控制字符极罕见,不处理(与原 sed fallback 一致)。
|
|
30
|
+
local escaped="${value//\\/\\\\}"
|
|
31
|
+
escaped="${escaped//\"/\\\"}"
|
|
35
32
|
printf '"%s"' "$escaped"
|
|
36
33
|
}
|
|
37
34
|
|
|
@@ -90,23 +87,33 @@ safe_delete_resolve_os_tmp_dir() {
|
|
|
90
87
|
|
|
91
88
|
safe_delete_abs_path() {
|
|
92
89
|
local p="$1"
|
|
93
|
-
local abs
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return 0
|
|
98
|
-
}
|
|
99
|
-
elif command -v python >/dev/null 2>&1; then
|
|
100
|
-
abs="$(python -c 'import os,sys; print(os.path.abspath(sys.argv[1]))' "$p" 2>/dev/null)" && {
|
|
101
|
-
safe_delete_to_unix_path_if_possible "$abs"
|
|
102
|
-
return 0
|
|
103
|
-
}
|
|
104
|
-
fi
|
|
90
|
+
local abs dir base
|
|
91
|
+
# 纯 bash fallback:cd -P 解析物理路径(含符号链接),cygpath 转 Unix 风格。
|
|
92
|
+
# 不依赖 python:Windows Store python3 stub 每次调用 1-4 秒且 stdout 为空,
|
|
93
|
+
# WorkBuddy 沙箱自带 python 启动也可能慢/卡,fallback 更快更可靠。
|
|
105
94
|
case "$p" in
|
|
106
95
|
/*|[A-Za-z]:/*) abs="$p" ;;
|
|
107
|
-
|
|
96
|
+
# ${PWD%/} 去掉 $PWD 尾部斜杠:$PWD=/ 时裸拼接会产出 //foo,
|
|
97
|
+
# 被 Windows 解释为 UNC \\foo 触发网络解析卡死 120s。
|
|
98
|
+
*) abs="${PWD%/}/$p" ;;
|
|
99
|
+
esac
|
|
100
|
+
# 用 bash 内置参数展开替代 dirname/basename 外部命令,
|
|
101
|
+
# 避免 Git Bash 每次 fork 子 shell ~200ms 的开销。
|
|
102
|
+
case "$abs" in
|
|
103
|
+
*/*) dir="${abs%/*}"; [ -z "$dir" ] && dir="/" ;;
|
|
104
|
+
*) dir="." ;;
|
|
105
|
+
esac
|
|
106
|
+
base="${abs##*/}"
|
|
107
|
+
if [ "$dir" != "." ] && [ "$dir" != "$abs" ]; then
|
|
108
|
+
abs="$(CDPATH= cd -P -- "$dir" 2>/dev/null && printf '%s/%s' "${PWD%/}" "$base" || printf '%s' "$abs")"
|
|
109
|
+
fi
|
|
110
|
+
# 出口兜底:cd -P -- // 在部分 shell 下 $PWD 会保留为 //(POSIX 允许实现选择
|
|
111
|
+
# // 开头的根),上面两处 ${PWD%/} 只剥一层,//foo 这类输入仍可能漏网。
|
|
112
|
+
# 在这里统一收口:规范化单组件 // 前缀为 /,保留 //server/share 多组件 UNC 形态。
|
|
113
|
+
case "$abs" in
|
|
114
|
+
//*/*) ;;
|
|
115
|
+
//*) abs="/${abs#//}" ;;
|
|
108
116
|
esac
|
|
109
|
-
abs="$(CDPATH= cd -P -- "$(dirname "$abs")" 2>/dev/null && printf '%s/%s' "$PWD" "$(basename "$abs")" || printf '%s' "$abs")"
|
|
110
117
|
safe_delete_to_unix_path_if_possible "$abs"
|
|
111
118
|
}
|
|
112
119
|
|
|
@@ -161,8 +168,9 @@ safe_delete_is_under_os_tmp_dir() {
|
|
|
161
168
|
case "$OS" in
|
|
162
169
|
MINGW*|MSYS*|CYGWIN*)
|
|
163
170
|
local abs_lower tmp_lower
|
|
164
|
-
|
|
165
|
-
|
|
171
|
+
# 用 bash 4+ 内置小写转换 ${var,,} 替代 printf|tr,避免 fork 子 shell。
|
|
172
|
+
abs_lower="${abs,,}"
|
|
173
|
+
tmp_lower="${tmp_dir,,}"
|
|
166
174
|
case "$abs_lower" in
|
|
167
175
|
"$tmp_lower"/*) return 0 ;;
|
|
168
176
|
esac
|
|
@@ -312,6 +320,41 @@ GT_ARCH="$(_gt_arch)"
|
|
|
312
320
|
GT_EXT=""
|
|
313
321
|
[ "$GT_PLATFORM" = "win32" ] && GT_EXT=".exe"
|
|
314
322
|
|
|
323
|
+
# 将路径转换为 Windows 风格绝对路径,供 native-trash binary 使用。
|
|
324
|
+
# 不依赖 cygpath(沙箱 PATH 可能不含 PortableGit/usr/bin)。
|
|
325
|
+
# 失败时返回非 0,由调用方 fail-closed。
|
|
326
|
+
safe_delete_to_windows_path() {
|
|
327
|
+
local p="$1"
|
|
328
|
+
local out
|
|
329
|
+
|
|
330
|
+
case "$p" in
|
|
331
|
+
/[A-Za-z]/*)
|
|
332
|
+
local drive="${p:1:1}"
|
|
333
|
+
local rest="${p:3}"
|
|
334
|
+
out="$(printf '%s:\\%s' "$drive" "${rest//\//\\}")"
|
|
335
|
+
;;
|
|
336
|
+
[A-Za-z]:/*)
|
|
337
|
+
out="$(printf '%s' "${p//\//\\}")"
|
|
338
|
+
;;
|
|
339
|
+
//*)
|
|
340
|
+
local rest="${p:2}"
|
|
341
|
+
out="$(printf '\\\\%s' "${rest//\//\\}")"
|
|
342
|
+
;;
|
|
343
|
+
*)
|
|
344
|
+
if command -v cygpath >/dev/null 2>&1; then
|
|
345
|
+
out="$(cygpath -w "$p" 2>/dev/null)"
|
|
346
|
+
fi
|
|
347
|
+
;;
|
|
348
|
+
esac
|
|
349
|
+
|
|
350
|
+
# 契约:返回非 0 表示转换失败。校验输出是合法 Windows 绝对路径(盘符或 UNC),
|
|
351
|
+
# 避免 cygpath fallback 对相对路径静默通过导致 native-trash exit 2 才暴露问题。
|
|
352
|
+
case "$out" in
|
|
353
|
+
[A-Za-z]:[\\]*|\\\\*) printf '%s' "$out"; return 0 ;;
|
|
354
|
+
*) return 1 ;;
|
|
355
|
+
esac
|
|
356
|
+
}
|
|
357
|
+
|
|
315
358
|
if [ -n "${GENIE_TRASH_DIR:-}" ]; then
|
|
316
359
|
TRASH_BIN="${GENIE_TRASH_DIR}/${GT_PLATFORM}-${GT_ARCH}${GT_EXT}"
|
|
317
360
|
else
|
|
@@ -322,7 +365,10 @@ trash_windows() {
|
|
|
322
365
|
local p="$1"
|
|
323
366
|
# 转为 Windows 风格路径(powershell 不接受 / 分隔的路径)
|
|
324
367
|
local wp
|
|
325
|
-
wp="$(
|
|
368
|
+
wp="$(safe_delete_to_windows_path "$p")" || {
|
|
369
|
+
_safe_delete_diag "cannot convert path to Windows absolute path: path=$p"
|
|
370
|
+
return 1
|
|
371
|
+
}
|
|
326
372
|
# 转义路径中的单引号:' → ''(PowerShell 单引号字符串内的转义方式)
|
|
327
373
|
local wp_escaped="${wp//\'/\'\'}"
|
|
328
374
|
|
|
@@ -363,26 +409,37 @@ trash_windows() {
|
|
|
363
409
|
}
|
|
364
410
|
|
|
365
411
|
trash_one() {
|
|
366
|
-
local p
|
|
412
|
+
local p
|
|
367
413
|
local native_failed=0
|
|
368
|
-
# 转为绝对路径:genie-trash binary
|
|
369
|
-
# /
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
*) p="$PWD/$p" ;;
|
|
373
|
-
esac
|
|
414
|
+
# 转为绝对路径:genie-trash binary 拒绝相对路径。
|
|
415
|
+
# 复用 safe_delete_abs_path(内含 // → / 规范化)而不是重复写一份绝对化逻辑,
|
|
416
|
+
# 避免两处路径解析出现分叉、其中一处漏掉 UNC 卡死修复。
|
|
417
|
+
p="$(safe_delete_abs_path "$1")"
|
|
374
418
|
# 优先 native binary
|
|
375
419
|
if [ -x "$TRASH_BIN" ]; then
|
|
376
420
|
local native_path="$p"
|
|
377
421
|
if [ "$GT_PLATFORM" = "win32" ]; then
|
|
378
|
-
native_path="$(
|
|
422
|
+
native_path="$(safe_delete_to_windows_path "$p")" || {
|
|
423
|
+
_safe_delete_diag "cannot convert path to Windows absolute path: path=$p"
|
|
424
|
+
return 1
|
|
425
|
+
}
|
|
379
426
|
fi
|
|
380
|
-
local native_out
|
|
427
|
+
local native_out existed=1
|
|
428
|
+
[ -e "$p" ] || [ -L "$p" ] || existed=0
|
|
381
429
|
native_out="$("$TRASH_BIN" "$native_path" 2>&1)"
|
|
382
430
|
local native_rc=$?
|
|
383
431
|
if [ $native_rc -eq 0 ]; then
|
|
384
432
|
return 0
|
|
385
433
|
fi
|
|
434
|
+
# Windows 上 trash crate 的 IFileOperation::GetAnyOperationsAborted 会误报 true,
|
|
435
|
+
# 但 PerformOperations 实际已删除文件。以文件系统事实为准:调用前在、调用后没了 = 删除成功。
|
|
436
|
+
# 仅 Windows 启用:根因是 Win32 独有 API,非 Windows 上失败原因未知,不应静默吞掉。
|
|
437
|
+
# 参考 https://github.com/maxkagamine/wsl-tools 的 recycle_bin.rs
|
|
438
|
+
# 保持静默:与 node-safe-delete-shim.cjs / sitecustomize.py 一致,避免 stderr 里的
|
|
439
|
+
# exit code / native ERROR 文本被上层 Bash 工具当作失败信号喂给模型。
|
|
440
|
+
if [ "$GT_PLATFORM" = "win32" ] && [ $existed -eq 1 ] && [ ! -e "$p" ] && [ ! -L "$p" ]; then
|
|
441
|
+
return 0
|
|
442
|
+
fi
|
|
386
443
|
_safe_delete_diag "genie-trash failed: exit=$native_rc bin=$TRASH_BIN path=$p stderr=$(_safe_delete_one_line "$native_out")"
|
|
387
444
|
native_failed=1
|
|
388
445
|
else
|
|
@@ -868,6 +868,7 @@ if _SESSION_ID:
|
|
|
868
868
|
global _trash_bin_available
|
|
869
869
|
if _trash_bin_available is False:
|
|
870
870
|
return False
|
|
871
|
+
existed = os.path.lexists(abs_path)
|
|
871
872
|
try:
|
|
872
873
|
result = subprocess.run(
|
|
873
874
|
[_get_trash_bin(), abs_path],
|
|
@@ -876,6 +877,13 @@ if _SESSION_ID:
|
|
|
876
877
|
if result.returncode == 0:
|
|
877
878
|
_trash_bin_available = True
|
|
878
879
|
return True
|
|
880
|
+
# Windows 上 trash crate 的 IFileOperation::GetAnyOperationsAborted 会误报 true,
|
|
881
|
+
# 但 PerformOperations 实际已删除文件。以文件系统事实为准:调用前在、调用后没了 = 删除成功。
|
|
882
|
+
# 仅 Windows 启用:根因是 Win32 独有 API,非 Windows 上失败原因未知,不应静默吞掉。
|
|
883
|
+
# 参考 https://github.com/maxkagamine/wsl-tools 的 recycle_bin.rs
|
|
884
|
+
if sys.platform == "win32" and existed and not os.path.lexists(abs_path):
|
|
885
|
+
_trash_bin_available = True
|
|
886
|
+
return True
|
|
879
887
|
# binary 已启动但对该路径失败:抛错,不降级真删
|
|
880
888
|
raise OSError(
|
|
881
889
|
"[safe-delete] 操作失败: %s"
|
|
@@ -888,6 +896,10 @@ if _SESSION_ID:
|
|
|
888
896
|
# 不缓存:权限问题可能是临时状态(如 chmod +x 后恢复)
|
|
889
897
|
return False
|
|
890
898
|
except subprocess.TimeoutExpired:
|
|
899
|
+
# 与 exit 1 同样检查:超时也可能文件已被删除
|
|
900
|
+
if sys.platform == "win32" and existed and not os.path.lexists(abs_path):
|
|
901
|
+
_trash_bin_available = True
|
|
902
|
+
return True
|
|
891
903
|
return False # 超时是临时问题,不永久禁用 native binary
|
|
892
904
|
except OSError:
|
|
893
905
|
raise
|