tmuxclihook 0.1.1 → 0.2.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/README.md +46 -12
- package/bin/cli.js +158 -52
- package/package.json +7 -3
- package/scripts/adapters/claude.sh +127 -0
- package/scripts/adapters/codex.sh +117 -0
- package/scripts/adapters/opencode.sh +169 -0
- package/scripts/install-claude-hooks.sh +19 -0
- package/scripts/install-codex-hooks.sh +19 -0
- package/scripts/install-opencode-hooks.sh +19 -0
- package/scripts/lib-install-hooks.sh +26 -0
- package/scripts/lib-tmux-ai-log.sh +81 -0
- package/scripts/lib-tmux-ai-status.sh +521 -0
- package/scripts/tmux-ai-esc +24 -0
- package/scripts/tmux-ai-monitor +190 -0
- package/scripts/tmux-ai-status +265 -0
- package/src/adapters-meta.js +90 -53
- package/src/purge.js +7 -1
- package/src/source.js +140 -0
- package/src/symlink.js +20 -19
- package/src/tmuxconf.js +66 -21
- package/src/update.js +39 -0
- package/tmux-ai-hooks-status.tmux +114 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# adapters/opencode.sh — opencode (sst/opencode) 工具适配器
|
|
3
|
+
#
|
|
4
|
+
# 由 lib-tmux-ai-status.sh 在确定 TOOL_ID=opencode 后 source。契约见 adapters/claude.sh 头注释。
|
|
5
|
+
#
|
|
6
|
+
# ⚠️ opencode 与 Claude/Codex 的关键差异(本 adapter 声明的语义):
|
|
7
|
+
# - 无独立 hooks 配置文件:opencode 用 JS/TS **plugin** 模型(~/.config/opencode/plugins/),
|
|
8
|
+
# 不是 Claude 的 settings.json / Codex 的 hooks.json。ADAPTER_HOOKS_FILE 指向本插件生成的
|
|
9
|
+
# 单个 plugin 文件(.ts),install/uninstall 是整文件写入/删除,不做 jq 合并。
|
|
10
|
+
# - plugin 内部订阅 opencode 的命名 hook(chat.message / tool.execute.before/after)+ 通用
|
|
11
|
+
# event 流(session.created/idle、permission.*、question.*),拼成 Claude 规范 JSON 后
|
|
12
|
+
# pipe 给本仓库 tmux-ai-status,核心引擎(tmux-ai-status 的事件 case)零改动。
|
|
13
|
+
# - opencode 有两套独立「问用户」系统,均经 event 流(非命名 hook,见 issue #7006
|
|
14
|
+
# permission.ask hook 从不触发):Permission(bash/edit 授权)发 permission.asked/replied
|
|
15
|
+
# → `!`;Question(AskUserQuestion 选项弹窗)发 question.asked/replied/rejected,adapter
|
|
16
|
+
# 把 tool_name 填 "AskUserQuestion" → core 派生 `?`。两套的 request_id 用事件 .id/.requestID
|
|
17
|
+
# 精确配对解除。
|
|
18
|
+
# - 无 SessionEnd:退出无 hook,由 tmux-ai-monitor 做进程检测清理(同 codex)。
|
|
19
|
+
# - SessionStart 即时:session.created 在会话建立时立即触发(不同于 codex 的 deferred)。
|
|
20
|
+
|
|
21
|
+
# 6 个规范生命周期事件 + 精确解除审批态的内部 PermissionResolved 事件。
|
|
22
|
+
ADAPTER_EVENTS="SessionStart UserPromptSubmit PreToolUse PermissionRequest PermissionResolved PostToolUse Stop"
|
|
23
|
+
|
|
24
|
+
# 前台进程名:opencode 二进制无 arch 后缀(区别于 codex 的 codex-aarch64-* 等)
|
|
25
|
+
ADAPTER_PROCESS_NAMES="opencode"
|
|
26
|
+
|
|
27
|
+
# plugin 文件路径(XDG 全局目录,OPENCODE_CONFIG_DIR 可覆盖)
|
|
28
|
+
ADAPTER_HOOKS_FILE="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/plugins/tmux-ai-status.ts"
|
|
29
|
+
|
|
30
|
+
# opencode 无 SessionEnd → 退出由 tmux-ai-monitor 做进程检测
|
|
31
|
+
ADAPTER_HAS_SESSION_END="false"
|
|
32
|
+
# session.created 即时触发
|
|
33
|
+
ADAPTER_SESSION_START_TIMING="immediate"
|
|
34
|
+
# permission.asked / question.asked 均携带请求 ID(.id),不需要无 ID sentinel。
|
|
35
|
+
ADAPTER_HOLD_UNMATCHED_PERMISSION="false"
|
|
36
|
+
|
|
37
|
+
# 自修复/TUI 调用的安装脚本
|
|
38
|
+
ADAPTER_INSTALLER="${_LIB_DIR}/install-opencode-hooks.sh"
|
|
39
|
+
|
|
40
|
+
_OPENCODE_MARKER="tmux-ai-status (managed by tmux-ai-hooks-status)"
|
|
41
|
+
|
|
42
|
+
# 完整性检查:plugin 文件存在且含本插件标记 + 指向当前仓库的 hook 脚本路径。
|
|
43
|
+
adapter_check_integrity() {
|
|
44
|
+
[ -f "$ADAPTER_HOOKS_FILE" ] || return 1
|
|
45
|
+
grep -q "$_OPENCODE_MARKER" "$ADAPTER_HOOKS_FILE" 2>/dev/null || return 1
|
|
46
|
+
grep -q "${_LIB_DIR}/tmux-ai-status" "$ADAPTER_HOOKS_FILE" 2>/dev/null || return 1
|
|
47
|
+
grep -q 'case "permission.asked"' "$ADAPTER_HOOKS_FILE" 2>/dev/null || return 1
|
|
48
|
+
grep -q 'case "question.asked"' "$ADAPTER_HOOKS_FILE" 2>/dev/null || return 1
|
|
49
|
+
grep -q 'case "permission.replied"' "$ADAPTER_HOOKS_FILE" 2>/dev/null || return 1
|
|
50
|
+
grep -q 'request_id: p.requestID' "$ADAPTER_HOOKS_FILE" 2>/dev/null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# 生成 plugin 文件内容。$1 = tmux-ai-status 绝对路径。
|
|
54
|
+
_opencode_plugin_content() {
|
|
55
|
+
local hook_script="$1"
|
|
56
|
+
cat <<EOF
|
|
57
|
+
// ${_OPENCODE_MARKER} — do not edit, changes will be overwritten on reinstall
|
|
58
|
+
// 由 install-opencode-hooks.sh 生成。把 opencode 原生事件映射为 Claude 规范事件,
|
|
59
|
+
// 拼规范 JSON 喂给 tmux-ai-status,核心引擎(tmux-ai-status 的事件 case)零改动。
|
|
60
|
+
|
|
61
|
+
const HOOK_SCRIPT = "${hook_script}"
|
|
62
|
+
|
|
63
|
+
export const TmuxAiStatus = async ({ \$ }) => {
|
|
64
|
+
const fire = (eventName, payload) => {
|
|
65
|
+
const json = JSON.stringify(payload || {})
|
|
66
|
+
return \$\`printf '%s' \${json} | \${HOOK_SCRIPT} opencode \${eventName}\`.quiet().nothrow()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// opencode 用「命名 hook」派发消息/工具执行点(chat.message / tool.execute.*),
|
|
70
|
+
// 权限与提问走通用 event 流(permission.* / question.*),会话生命周期也走 event 流
|
|
71
|
+
// (session.created/idle)。分开映射:
|
|
72
|
+
return {
|
|
73
|
+
// 用户提交提示词
|
|
74
|
+
"chat.message": async (input) => {
|
|
75
|
+
await fire("UserPromptSubmit", { session_id: input.sessionID || "" })
|
|
76
|
+
},
|
|
77
|
+
// 工具开始执行
|
|
78
|
+
"tool.execute.before": async (input) => {
|
|
79
|
+
await fire("PreToolUse", {
|
|
80
|
+
session_id: input.sessionID || "",
|
|
81
|
+
tool_name: input.tool || "",
|
|
82
|
+
tool_use_id: input.callID || "",
|
|
83
|
+
})
|
|
84
|
+
},
|
|
85
|
+
// 工具执行完毕
|
|
86
|
+
"tool.execute.after": async (input) => {
|
|
87
|
+
await fire("PostToolUse", {
|
|
88
|
+
session_id: input.sessionID || "",
|
|
89
|
+
tool_name: input.tool || "",
|
|
90
|
+
tool_use_id: input.callID || "",
|
|
91
|
+
})
|
|
92
|
+
},
|
|
93
|
+
// 通用事件流。opencode 有两套独立「问用户」系统,各发各的事件(均经 event 流,
|
|
94
|
+
// 不经命名 hook —— permission.ask 命名 hook 上游从不触发,见 issue #7006):
|
|
95
|
+
//
|
|
96
|
+
// 1. Permission(bash/edit 等授权):
|
|
97
|
+
// permission.asked → PermissionRequest(tool_name≠AskUserQuestion → \`!\`)
|
|
98
|
+
// properties: {id, sessionID, permission=工具名, tool:{callID}}
|
|
99
|
+
// permission.replied → PermissionResolved
|
|
100
|
+
// properties: {sessionID, requestID, reply: once|always|reject}
|
|
101
|
+
//
|
|
102
|
+
// 2. Question(AskUserQuestion 选项弹窗):
|
|
103
|
+
// question.asked → PermissionRequest(tool_name=AskUserQuestion → \`?\`)
|
|
104
|
+
// properties: {id, sessionID, questions[], tool:{callID}}
|
|
105
|
+
// question.replied → PermissionResolved(用户作答)
|
|
106
|
+
// question.rejected → PermissionResolved(用户 dismiss)
|
|
107
|
+
// properties: {sessionID, requestID, ...}
|
|
108
|
+
//
|
|
109
|
+
// 两套的 request_id 均取事件的 .id(ask)/.requestID(reply),core 精确配对解除。
|
|
110
|
+
event: async ({ event }) => {
|
|
111
|
+
const p = event.properties || {}
|
|
112
|
+
switch (event.type) {
|
|
113
|
+
case "session.created":
|
|
114
|
+
return fire("SessionStart", { session_id: p.sessionID || p.id || "" })
|
|
115
|
+
case "session.idle":
|
|
116
|
+
return fire("Stop", { session_id: p.sessionID || p.id || "" })
|
|
117
|
+
case "permission.asked":
|
|
118
|
+
return fire("PermissionRequest", {
|
|
119
|
+
session_id: p.sessionID || "",
|
|
120
|
+
tool_name: p.permission || "",
|
|
121
|
+
tool_use_id: p.id || "",
|
|
122
|
+
})
|
|
123
|
+
case "question.asked":
|
|
124
|
+
return fire("PermissionRequest", {
|
|
125
|
+
session_id: p.sessionID || "",
|
|
126
|
+
tool_name: "AskUserQuestion",
|
|
127
|
+
tool_use_id: p.id || "",
|
|
128
|
+
})
|
|
129
|
+
case "permission.replied":
|
|
130
|
+
case "question.replied":
|
|
131
|
+
case "question.rejected":
|
|
132
|
+
return fire("PermissionResolved", {
|
|
133
|
+
session_id: p.sessionID || "",
|
|
134
|
+
request_id: p.requestID || "",
|
|
135
|
+
reply: p.reply || "",
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
EOF
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# 安装:写出/覆盖 plugin 文件。整文件替换(非合并)——同目录下他人 plugin 文件不受影响。
|
|
145
|
+
adapter_install_hooks() {
|
|
146
|
+
mkdir -p "$(dirname "$ADAPTER_HOOKS_FILE")" 2>/dev/null || true
|
|
147
|
+
local hook_script="${_LIB_DIR}/tmux-ai-status"
|
|
148
|
+
local content
|
|
149
|
+
content=$(_opencode_plugin_content "$hook_script")
|
|
150
|
+
_install_atomic_write "$content" "$ADAPTER_HOOKS_FILE"
|
|
151
|
+
echo "opencode plugin installed to $ADAPTER_HOOKS_FILE"
|
|
152
|
+
echo "Events: $ADAPTER_EVENTS"
|
|
153
|
+
echo ""
|
|
154
|
+
echo "⚠️ 下次启动 opencode 会自动加载该 plugin(本地文件插件无需额外授信)。"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# 卸载:仅当文件由本插件生成(含 marker)时删除,避免误删用户自建同名 plugin。
|
|
158
|
+
adapter_uninstall_hooks() {
|
|
159
|
+
if [ -f "$ADAPTER_HOOKS_FILE" ]; then
|
|
160
|
+
if grep -q "$_OPENCODE_MARKER" "$ADAPTER_HOOKS_FILE" 2>/dev/null; then
|
|
161
|
+
rm -f "$ADAPTER_HOOKS_FILE"
|
|
162
|
+
echo "opencode plugin uninstalled (removed $ADAPTER_HOOKS_FILE)"
|
|
163
|
+
else
|
|
164
|
+
echo "opencode plugin: $ADAPTER_HOOKS_FILE 非本插件生成,跳过删除"
|
|
165
|
+
fi
|
|
166
|
+
else
|
|
167
|
+
echo "opencode plugin: nothing to uninstall ($ADAPTER_HOOKS_FILE not found)"
|
|
168
|
+
fi
|
|
169
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# install-claude-hooks.sh — Claude Code hooks 安装/卸载 wrapper
|
|
3
|
+
# 用法: install-claude-hooks.sh [uninstall]
|
|
4
|
+
#
|
|
5
|
+
# 薄 wrapper:source 公共骨架 + claude adapter,dispatch 到 adapter_install_hooks /
|
|
6
|
+
# adapter_uninstall_hooks。实际逻辑在 scripts/adapters/claude.sh。
|
|
7
|
+
|
|
8
|
+
set -o errexit
|
|
9
|
+
set -o pipefail
|
|
10
|
+
|
|
11
|
+
_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
12
|
+
source "${_LIB_DIR}/lib-install-hooks.sh"
|
|
13
|
+
source "${_LIB_DIR}/adapters/claude.sh"
|
|
14
|
+
|
|
15
|
+
if [ "${1:-install}" = "uninstall" ]; then
|
|
16
|
+
adapter_uninstall_hooks
|
|
17
|
+
else
|
|
18
|
+
adapter_install_hooks
|
|
19
|
+
fi
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# install-codex-hooks.sh — Codex CLI hooks 安装/卸载 wrapper(需 codex ≥ 0.144)
|
|
3
|
+
# 用法: install-codex-hooks.sh [uninstall]
|
|
4
|
+
#
|
|
5
|
+
# 薄 wrapper:source 公共骨架 + codex adapter,dispatch 到 adapter_install_hooks /
|
|
6
|
+
# adapter_uninstall_hooks。实际逻辑在 scripts/adapters/codex.sh。
|
|
7
|
+
|
|
8
|
+
set -o errexit
|
|
9
|
+
set -o pipefail
|
|
10
|
+
|
|
11
|
+
_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
12
|
+
source "${_LIB_DIR}/lib-install-hooks.sh"
|
|
13
|
+
source "${_LIB_DIR}/adapters/codex.sh"
|
|
14
|
+
|
|
15
|
+
if [ "${1:-install}" = "uninstall" ]; then
|
|
16
|
+
adapter_uninstall_hooks
|
|
17
|
+
else
|
|
18
|
+
adapter_install_hooks
|
|
19
|
+
fi
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# install-opencode-hooks.sh — opencode plugin 安装/卸载 wrapper
|
|
3
|
+
# 用法: install-opencode-hooks.sh [uninstall]
|
|
4
|
+
#
|
|
5
|
+
# 薄 wrapper:source 公共骨架 + opencode adapter,dispatch 到 adapter_install_hooks /
|
|
6
|
+
# adapter_uninstall_hooks。实际逻辑在 scripts/adapters/opencode.sh。
|
|
7
|
+
|
|
8
|
+
set -o errexit
|
|
9
|
+
set -o pipefail
|
|
10
|
+
|
|
11
|
+
_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
12
|
+
source "${_LIB_DIR}/lib-install-hooks.sh"
|
|
13
|
+
source "${_LIB_DIR}/adapters/opencode.sh"
|
|
14
|
+
|
|
15
|
+
if [ "${1:-install}" = "uninstall" ]; then
|
|
16
|
+
adapter_uninstall_hooks
|
|
17
|
+
else
|
|
18
|
+
adapter_install_hooks
|
|
19
|
+
fi
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# lib-install-hooks.sh — hooks 安装公共骨架
|
|
3
|
+
#
|
|
4
|
+
# 被 install-<tool>-hooks.sh wrapper source。提供工具无关的依赖检查与原子写;
|
|
5
|
+
# 工具特定的 jq 合并逻辑在各 adapter 的 adapter_install_hooks/adapter_uninstall_hooks 中。
|
|
6
|
+
#
|
|
7
|
+
# 契约:wrapper source 本文件 + adapters/<tool>.sh 后,调 adapter_install_hooks 或
|
|
8
|
+
# adapter_uninstall_hooks。adapter 的 install 函数用 _install_require_jq / _install_atomic_write。
|
|
9
|
+
|
|
10
|
+
# 依赖检查:jq 是唯一硬依赖
|
|
11
|
+
_install_require_jq() {
|
|
12
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
13
|
+
echo "ERROR: jq is required. Install with: brew install jq" >&2
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# 原子写入:mktemp 与目标同目录 → 同文件系统 → mv 原子。
|
|
19
|
+
# 避免与 AI CLI 并发写产生竞态导致 hooks 字段丢失。
|
|
20
|
+
_install_atomic_write() {
|
|
21
|
+
local content="$1" target="$2" tmp
|
|
22
|
+
mkdir -p "$(dirname "$target")" 2>/dev/null || true
|
|
23
|
+
tmp=$(mktemp "$(dirname "$target")/.hooks.XXXXXX")
|
|
24
|
+
printf '%s\n' "$content" > "$tmp"
|
|
25
|
+
mv -f "$tmp" "$target"
|
|
26
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# lib-tmux-ai-log.sh: 轻量日志模块 — 直接 O_APPEND 追加 + 原子轮转
|
|
3
|
+
# 被 lib-tmux-ai-status.sh source
|
|
4
|
+
# 调用方需设置: TOOL_ID ("claude")、EVENT、_pane_loc
|
|
5
|
+
|
|
6
|
+
AI_LOG_FILE="/tmp/tmux-ai-status.log"
|
|
7
|
+
AI_LOG_MAX_SIZE=524288 # 512KB 触发轮转
|
|
8
|
+
AI_LOG_KEEP_SIZE=262144 # 轮转后保留尾部 256KB
|
|
9
|
+
AI_LOG_LOCK_DIR="${AI_LOG_FILE}.lock"
|
|
10
|
+
|
|
11
|
+
# 原子轮转(mkdir 作为互斥锁,多进程安全)
|
|
12
|
+
_ai_log_rotate() {
|
|
13
|
+
mkdir "$AI_LOG_LOCK_DIR" 2>/dev/null || return 0
|
|
14
|
+
local cur_size
|
|
15
|
+
cur_size=$(wc -c < "$AI_LOG_FILE" 2>/dev/null || echo 0)
|
|
16
|
+
if [ "${cur_size:-0}" -gt "$AI_LOG_MAX_SIZE" ]; then
|
|
17
|
+
local tmp="${AI_LOG_FILE}.tmp.$$"
|
|
18
|
+
if tail -c "$AI_LOG_KEEP_SIZE" "$AI_LOG_FILE" > "$tmp" 2>/dev/null; then
|
|
19
|
+
cat "$tmp" > "$AI_LOG_FILE"
|
|
20
|
+
fi
|
|
21
|
+
rm -f "$tmp"
|
|
22
|
+
fi
|
|
23
|
+
rmdir "$AI_LOG_LOCK_DIR" 2>/dev/null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_ai_log() {
|
|
27
|
+
local msg="[$(date '+%Y-%m-%dT%H:%M:%S')] [${TOOL_ID:-?}] [${_pane_loc:-${TMUX_PANE:-?}}] [${EVENT:-?}] $*"
|
|
28
|
+
|
|
29
|
+
# 首次创建时限制权限(避免 /tmp 下日志对其它用户可读)
|
|
30
|
+
[ -f "$AI_LOG_FILE" ] || (umask 077; : >> "$AI_LOG_FILE")
|
|
31
|
+
|
|
32
|
+
# POSIX O_APPEND 对单行 ≤PIPE_BUF (≥4096B) 写入保证原子,多进程无需额外锁
|
|
33
|
+
printf '%s\n' "$msg" >> "$AI_LOG_FILE" 2>/dev/null
|
|
34
|
+
|
|
35
|
+
# 按需轮转(单次 wc -c 开销微小,hook 每次只调一次 _ai_log)
|
|
36
|
+
local fsize
|
|
37
|
+
fsize=$(wc -c < "$AI_LOG_FILE" 2>/dev/null || echo 0)
|
|
38
|
+
[ "${fsize:-0}" -gt "$AI_LOG_MAX_SIZE" ] && _ai_log_rotate
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# 事件块日志:头行 + 缩进详情行,一次写入保证原子
|
|
42
|
+
# 用法: _ai_log_event PREV_STATUS CURR_STATUS FLAGS_STR [detail_line...]
|
|
43
|
+
_ai_log_event() {
|
|
44
|
+
local prev="${1:-·}" curr="${2:-·}" flags="$3"; shift 3
|
|
45
|
+
[ -z "$prev" ] && prev="·"
|
|
46
|
+
[ -z "$curr" ] && curr="·"
|
|
47
|
+
local transition
|
|
48
|
+
if [ "$prev" = "$curr" ]; then
|
|
49
|
+
transition="'${curr}'"
|
|
50
|
+
else
|
|
51
|
+
transition="'${prev}' → '${curr}'"
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
local header="[$(date '+%Y-%m-%dT%H:%M:%S')] [${TOOL_ID:-?}] [${_pane_loc:-${TMUX_PANE:-?}}] [${EVENT:-?}] ${transition} ${flags}"
|
|
55
|
+
|
|
56
|
+
[ -f "$AI_LOG_FILE" ] || (umask 077; : >> "$AI_LOG_FILE")
|
|
57
|
+
|
|
58
|
+
# 构造整块后一次 printf 追加,≤PIPE_BUF 原子
|
|
59
|
+
local block="${header}"$'\n'
|
|
60
|
+
local line
|
|
61
|
+
for line in "$@"; do
|
|
62
|
+
[ -z "$line" ] && continue
|
|
63
|
+
block+=" ${line}"$'\n'
|
|
64
|
+
done
|
|
65
|
+
printf '%s' "$block" >> "$AI_LOG_FILE" 2>/dev/null
|
|
66
|
+
|
|
67
|
+
local fsize
|
|
68
|
+
fsize=$(wc -c < "$AI_LOG_FILE" 2>/dev/null || echo 0)
|
|
69
|
+
[ "${fsize:-0}" -gt "$AI_LOG_MAX_SIZE" ] && _ai_log_rotate
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# input 超长截断:>2000B 保留前 2000B 加 "…+Nbytes"
|
|
73
|
+
_ai_log_truncate_input() {
|
|
74
|
+
local s="$1" max=2000
|
|
75
|
+
local n=${#s}
|
|
76
|
+
if [ "$n" -gt "$max" ]; then
|
|
77
|
+
printf '%s…+%dbytes' "${s:0:$max}" "$((n - max))"
|
|
78
|
+
else
|
|
79
|
+
printf '%s' "$s"
|
|
80
|
+
fi
|
|
81
|
+
}
|