@weotro/dx 0.1.21 → 0.2.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/README.md +76 -768
- package/bin/dx.js +100 -182
- package/example/.env.development +1 -0
- package/example/README.md +24 -0
- package/example/dx/scripts/build +2 -0
- package/example/dx/scripts/db +2 -0
- package/example/dx/scripts/deploy +2 -0
- package/example/dx/scripts/setup +2 -0
- package/example/dx/scripts/start +12 -0
- package/example/dx/scripts/test +2 -0
- package/lib/env.js +40 -257
- package/lib/logger.js +5 -238
- package/lib/setup.js +72 -0
- package/package.json +27 -13
- package/skills/ask_3th/SKILL.md +2 -0
- package/skills/delegate_3th/SKILL.md +4 -2
- package/skills/delegate_3th/scripts/delegate_3th.py +37 -41
- package/skills/delegate_3th/scripts/test_delegate_3th.py +43 -17
- package/bin/dx-with-version-env.js +0 -8
- package/lib/artifact-deploy/artifact-builder.js +0 -144
- package/lib/artifact-deploy/config.js +0 -180
- package/lib/artifact-deploy/remote-script.js +0 -301
- package/lib/artifact-deploy/remote-transport.js +0 -86
- package/lib/artifact-deploy.js +0 -70
- package/lib/backend-artifact-deploy/artifact-builder.js +0 -267
- package/lib/backend-artifact-deploy/config.js +0 -218
- package/lib/backend-artifact-deploy/path-utils.js +0 -18
- package/lib/backend-artifact-deploy/remote-phases.js +0 -14
- package/lib/backend-artifact-deploy/remote-result.js +0 -44
- package/lib/backend-artifact-deploy/remote-script.js +0 -507
- package/lib/backend-artifact-deploy/remote-transport.js +0 -123
- package/lib/backend-artifact-deploy/rollback.js +0 -5
- package/lib/backend-artifact-deploy/runtime-package.js +0 -46
- package/lib/backend-artifact-deploy.js +0 -91
- package/lib/backend-package.js +0 -674
- package/lib/cli/args.js +0 -38
- package/lib/cli/command-result.js +0 -1
- package/lib/cli/commands/contracts.js +0 -60
- package/lib/cli/commands/core.js +0 -533
- package/lib/cli/commands/db.js +0 -231
- package/lib/cli/commands/deploy.js +0 -175
- package/lib/cli/commands/env.js +0 -120
- package/lib/cli/commands/export.js +0 -39
- package/lib/cli/commands/package.js +0 -22
- package/lib/cli/commands/release.js +0 -55
- package/lib/cli/commands/stack.js +0 -427
- package/lib/cli/commands/start.js +0 -58
- package/lib/cli/commands/worktree.js +0 -145
- package/lib/cli/dx-cli.js +0 -1098
- package/lib/cli/flags.js +0 -123
- package/lib/cli/help-model.js +0 -234
- package/lib/cli/help-renderer.js +0 -137
- package/lib/cli/help-schema.js +0 -552
- package/lib/cli/help.js +0 -141
- package/lib/cli/index.js +0 -4
- package/lib/cli/nx-command.js +0 -13
- package/lib/confirm.js +0 -213
- package/lib/env-policy.js +0 -134
- package/lib/env-profile.js +0 -531
- package/lib/exec.js +0 -698
- package/lib/nx-ignore.js +0 -45
- package/lib/run-with-version-env.js +0 -163
- package/lib/sdk-build.js +0 -424
- package/lib/start-dev.js +0 -401
- package/lib/telegram-webhook.js +0 -431
- package/lib/validate-env.js +0 -317
- package/lib/vercel-deploy.js +0 -549
- package/lib/worktree.js +0 -1052
- package/skills/delegate_3th/scripts/__pycache__/delegate_3th.cpython-314.pyc +0 -0
- package/skills/delegate_3th/scripts/__pycache__/delegate_cc.cpython-314.pyc +0 -0
- package/skills/delegate_3th/scripts/__pycache__/test_delegate_cc.cpython-314.pyc +0 -0
package/lib/logger.js
CHANGED
|
@@ -1,239 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
export const logger = {
|
|
2
|
+
error(message) { process.stderr.write(`dx: ${message}\n`) },
|
|
3
|
+
warn(message) { process.stderr.write(`dx: ${message}\n`) },
|
|
4
|
+
info(message) { process.stdout.write(`${message}\n`) },
|
|
5
|
+
success(message) { process.stdout.write(`${message}\n`) },
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
export function sanitizeForLog(input) {
|
|
9
|
-
let text = input == null ? '' : String(input)
|
|
10
|
-
|
|
11
|
-
// CLI token args (vercel)
|
|
12
|
-
text = text.replace(/--token=("[^"]*"|'[^']*'|[^\s]+)/gi, '--token=***')
|
|
13
|
-
text = text.replace(/--token\s+("[^"]*"|'[^']*'|[^\s]+)/gi, '--token ***')
|
|
14
|
-
|
|
15
|
-
// Env style secrets
|
|
16
|
-
text = text.replace(/\bVERCEL_TOKEN=([^\s]+)/g, 'VERCEL_TOKEN=***')
|
|
17
|
-
text = text.replace(/\bTELEGRAM_BOT_TOKEN=([^\s]+)/g, 'TELEGRAM_BOT_TOKEN=***')
|
|
18
|
-
text = text.replace(
|
|
19
|
-
/\bTELEGRAM_BOT_WEBHOOK_SECRET=([^\s]+)/g,
|
|
20
|
-
'TELEGRAM_BOT_WEBHOOK_SECRET=***',
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
// Authorization bearer
|
|
24
|
-
text = text.replace(
|
|
25
|
-
/Authorization:\s*Bearer\s+([^\s]+)/gi,
|
|
26
|
-
'Authorization: Bearer ***',
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
// JSON-ish token fields
|
|
30
|
-
text = text.replace(/"token"\s*:\s*"[^"]*"/gi, '"token":"***"')
|
|
31
|
-
text = text.replace(
|
|
32
|
-
/("secret_token"\s*:\s*")([^"]*)(")/gi,
|
|
33
|
-
'$1***$3',
|
|
34
|
-
)
|
|
35
|
-
text = text.replace(/\bsecret_token=([^\s&]+)/gi, 'secret_token=***')
|
|
36
|
-
|
|
37
|
-
// Telegram bot token in URLs
|
|
38
|
-
text = text.replace(
|
|
39
|
-
/api\.telegram\.org\/bot([^/\s]+)(\/|$)/gi,
|
|
40
|
-
'api.telegram.org/bot***$2',
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
return text
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// 处理输出管道被关闭导致的 EPIPE 错误,避免进程在清理阶段崩溃
|
|
47
|
-
try {
|
|
48
|
-
const handleBrokenPipe = err => {
|
|
49
|
-
if (err && (err.code === 'EPIPE' || err.code === 'ERR_STREAM_WRITE_AFTER_END')) {
|
|
50
|
-
// 静默忽略,允许进程优雅退出
|
|
51
|
-
try {
|
|
52
|
-
/* noop */
|
|
53
|
-
} catch {}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (process?.stdout?.on) process.stdout.on('error', handleBrokenPipe)
|
|
57
|
-
if (process?.stderr?.on) process.stderr.on('error', handleBrokenPipe)
|
|
58
|
-
} catch {
|
|
59
|
-
/* 安全保护,忽略环境不支持的情况 */
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export class Logger {
|
|
63
|
-
constructor(options = {}) {
|
|
64
|
-
this.logLevel = options.level || 'info'
|
|
65
|
-
this.enableFile = options.enableFile || false
|
|
66
|
-
this.logDir = options.logDir || join(resolveProjectRoot(), 'dx', 'logs')
|
|
67
|
-
|
|
68
|
-
if (this.enableFile) {
|
|
69
|
-
this.ensureLogDir()
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// 确保日志目录存在
|
|
74
|
-
ensureLogDir() {
|
|
75
|
-
try {
|
|
76
|
-
mkdirSync(this.logDir, { recursive: true })
|
|
77
|
-
} catch (error) {
|
|
78
|
-
// 目录可能已存在,忽略错误
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// 格式化时间戳
|
|
83
|
-
formatTimestamp() {
|
|
84
|
-
return new Date().toLocaleString('zh-CN')
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// 基础日志方法
|
|
88
|
-
info(message, prefix = '🚀') {
|
|
89
|
-
const safeMessage = sanitizeForLog(message)
|
|
90
|
-
const output = `${prefix} ${safeMessage}`
|
|
91
|
-
console.log(output)
|
|
92
|
-
this.writeLog('info', safeMessage)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
success(message) {
|
|
96
|
-
const safeMessage = sanitizeForLog(message)
|
|
97
|
-
const output = `✅ ${safeMessage}`
|
|
98
|
-
console.log(output)
|
|
99
|
-
this.writeLog('success', safeMessage)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
warn(message) {
|
|
103
|
-
const safeMessage = sanitizeForLog(message)
|
|
104
|
-
const output = `⚠️ ${safeMessage}`
|
|
105
|
-
console.log(output)
|
|
106
|
-
this.writeLog('warn', safeMessage)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
error(message) {
|
|
110
|
-
const safeMessage = sanitizeForLog(message)
|
|
111
|
-
const output = `❌ ${safeMessage}`
|
|
112
|
-
console.log(output)
|
|
113
|
-
this.writeLog('error', safeMessage)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
debug(message) {
|
|
117
|
-
if (this.logLevel === 'debug') {
|
|
118
|
-
const safeMessage = sanitizeForLog(message)
|
|
119
|
-
const output = `🐛 ${safeMessage}`
|
|
120
|
-
console.log(output)
|
|
121
|
-
this.writeLog('debug', safeMessage)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// 步骤显示
|
|
126
|
-
step(message, stepNumber = null) {
|
|
127
|
-
const prefix = stepNumber ? `步骤 ${stepNumber}:` : '执行:'
|
|
128
|
-
const separator = '=================================='
|
|
129
|
-
|
|
130
|
-
const safeMessage = sanitizeForLog(message)
|
|
131
|
-
|
|
132
|
-
console.log(`\n${separator}`)
|
|
133
|
-
console.log(`🚀 ${prefix} ${safeMessage}`)
|
|
134
|
-
console.log(separator)
|
|
135
|
-
|
|
136
|
-
this.writeLog('step', `${prefix} ${safeMessage}`)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// 进度显示
|
|
140
|
-
progress(message) {
|
|
141
|
-
const safeMessage = sanitizeForLog(message)
|
|
142
|
-
process.stdout.write(`⌛ ${safeMessage}...`)
|
|
143
|
-
this.writeLog('progress', `开始: ${safeMessage}`)
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
progressDone() {
|
|
147
|
-
console.log(' 完成')
|
|
148
|
-
this.writeLog('progress', '完成')
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// 命令执行日志
|
|
152
|
-
command(command) {
|
|
153
|
-
const safeCommand = sanitizeForLog(command)
|
|
154
|
-
console.log(`💻 执行: ${safeCommand}`)
|
|
155
|
-
this.writeLog('command', safeCommand)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// 分隔符
|
|
159
|
-
separator() {
|
|
160
|
-
console.log(`\n${'='.repeat(50)}`)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// 表格显示
|
|
164
|
-
table(data, headers = []) {
|
|
165
|
-
if (data.length === 0) return
|
|
166
|
-
|
|
167
|
-
if (headers.length > 0) {
|
|
168
|
-
const safeHeaders = headers.map(h => sanitizeForLog(h))
|
|
169
|
-
console.log(`\n${safeHeaders.join('\t')}`)
|
|
170
|
-
console.log('-'.repeat(safeHeaders.join('\t').length))
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
data.forEach(row => {
|
|
174
|
-
if (Array.isArray(row)) {
|
|
175
|
-
console.log(row.map(cell => sanitizeForLog(cell)).join('\t'))
|
|
176
|
-
} else {
|
|
177
|
-
console.log(sanitizeForLog(row))
|
|
178
|
-
}
|
|
179
|
-
})
|
|
180
|
-
console.log()
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// 端口信息显示
|
|
184
|
-
ports(portInfo) {
|
|
185
|
-
console.log('\n📡 服务端口信息:')
|
|
186
|
-
portInfo.forEach(({ service, port, url }) => {
|
|
187
|
-
const safeService = sanitizeForLog(service)
|
|
188
|
-
const safeUrl = url ? sanitizeForLog(url) : ''
|
|
189
|
-
console.log(` ${safeService}: http://localhost:${port} ${safeUrl ? `(${safeUrl})` : ''}`)
|
|
190
|
-
})
|
|
191
|
-
console.log()
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// 写入日志文件
|
|
195
|
-
writeLog(level, message) {
|
|
196
|
-
if (!this.enableFile) return
|
|
197
|
-
|
|
198
|
-
try {
|
|
199
|
-
const safeMessage = sanitizeForLog(message)
|
|
200
|
-
const timestamp = this.formatTimestamp()
|
|
201
|
-
const logLine = `[${timestamp}] [${level.toUpperCase()}] ${safeMessage}\n`
|
|
202
|
-
|
|
203
|
-
const logFile = join(this.logDir, `dx-${new Date().toISOString().split('T')[0]}.log`)
|
|
204
|
-
writeFileSync(logFile, logLine, { flag: 'a', encoding: 'utf8' })
|
|
205
|
-
} catch (error) {
|
|
206
|
-
// 写入日志失败时静默处理,避免影响主流程
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// 创建子日志器
|
|
211
|
-
createChild(prefix) {
|
|
212
|
-
const childLogger = new Logger({
|
|
213
|
-
level: this.logLevel,
|
|
214
|
-
enableFile: this.enableFile,
|
|
215
|
-
logDir: this.logDir,
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
// 重写方法以添加前缀
|
|
219
|
-
const originalMethods = ['info', 'success', 'warn', 'error', 'debug']
|
|
220
|
-
originalMethods.forEach(method => {
|
|
221
|
-
const originalMethod = childLogger[method].bind(childLogger)
|
|
222
|
-
childLogger[method] = (message, customPrefix) => {
|
|
223
|
-
const finalPrefix = customPrefix || prefix
|
|
224
|
-
originalMethod(message, finalPrefix)
|
|
225
|
-
}
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
return childLogger
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// 导出默认实例
|
|
233
|
-
export const logger = new Logger()
|
|
234
|
-
|
|
235
|
-
// 导出带文件日志的实例
|
|
236
|
-
export const fileLogger = new Logger({ enableFile: true })
|
|
237
|
-
|
|
238
|
-
// 导出调试日志实例
|
|
239
|
-
export const debugLogger = new Logger({ level: 'debug', enableFile: true })
|
package/lib/setup.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { readdirSync, lstatSync, mkdirSync, copyFileSync, realpathSync, constants } from 'node:fs'
|
|
3
|
+
import { join, dirname, relative, resolve, sep } from 'node:path'
|
|
4
|
+
|
|
5
|
+
function git(root, args, allowOutsideRepository = false) {
|
|
6
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('GIT_')))
|
|
7
|
+
const result = spawnSync('git', args, {
|
|
8
|
+
cwd: root, env: { ...env, LC_ALL: 'C', GIT_OPTIONAL_LOCKS: '0' },
|
|
9
|
+
encoding: 'utf8', timeout: 10000, maxBuffer: 4 * 1024 * 1024,
|
|
10
|
+
})
|
|
11
|
+
if (allowOutsideRepository && result.status === 128 && result.stderr?.includes('not a git repository')) return null
|
|
12
|
+
if (result.error || result.status !== 0) {
|
|
13
|
+
throw new Error('基础 setup 无法读取 Git 工作目录;请检查 Git 已安装且当前仓库的 worktree 信息有效。')
|
|
14
|
+
}
|
|
15
|
+
return result.stdout
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function entryAt(path) {
|
|
19
|
+
try { return lstatSync(path) } catch (error) {
|
|
20
|
+
if (error.code === 'ENOENT') return null
|
|
21
|
+
throw error
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function prepareParent(root, destination) {
|
|
26
|
+
let directory = root
|
|
27
|
+
for (const part of relative(root, dirname(destination)).split(sep).filter(Boolean)) {
|
|
28
|
+
directory = join(directory, part)
|
|
29
|
+
try { mkdirSync(directory) } catch (error) {
|
|
30
|
+
if (error.code !== 'EEXIST') throw error
|
|
31
|
+
}
|
|
32
|
+
if (!lstatSync(directory).isDirectory()) {
|
|
33
|
+
throw new Error(`基础 setup 的目标父路径 ${directory} 不是普通目录;请移除路径冲突后重试。`)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function runBaseSetup(projectRoot) {
|
|
39
|
+
const top = git(projectRoot, ['rev-parse', '--show-toplevel'], true)
|
|
40
|
+
if (top === null) return
|
|
41
|
+
const destinationRoot = realpathSync(top.replace(/\r?\n$/, ''))
|
|
42
|
+
const records = git(projectRoot, ['worktree', 'list', '--porcelain', '-z'])
|
|
43
|
+
.split('\0\0').filter(Boolean).map(record => record.split('\0'))
|
|
44
|
+
// Git lists the main worktree first; a bare repository has no source checkout.
|
|
45
|
+
if (records[0]?.includes('bare')) return
|
|
46
|
+
const paths = records.map(record => record.find(field => field.startsWith('worktree '))?.slice(9))
|
|
47
|
+
if (!paths[0]) throw new Error('基础 setup 无法定位主 Git 工作目录;请检查 git worktree list。')
|
|
48
|
+
const sourceRoot = realpathSync(paths[0])
|
|
49
|
+
if (sourceRoot === destinationRoot) return
|
|
50
|
+
const worktrees = new Set(paths.filter(Boolean).map(path => resolve(path)))
|
|
51
|
+
|
|
52
|
+
function copyDirectory(directory) {
|
|
53
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
54
|
+
if (entry.name === '.git') continue
|
|
55
|
+
const source = join(directory, entry.name)
|
|
56
|
+
if (entry.isDirectory()) {
|
|
57
|
+
if (!worktrees.has(source)) copyDirectory(source)
|
|
58
|
+
} else if (entry.isFile() && entry.name.endsWith('.local')) {
|
|
59
|
+
const destination = join(destinationRoot, relative(sourceRoot, source))
|
|
60
|
+
prepareParent(destinationRoot, destination)
|
|
61
|
+
if (entryAt(destination)) continue
|
|
62
|
+
try { copyFileSync(source, destination, constants.COPYFILE_EXCL) } catch (error) {
|
|
63
|
+
if (error.code !== 'EEXIST') throw error
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
try { copyDirectory(sourceRoot) } catch (error) {
|
|
69
|
+
if (!error.code) throw error
|
|
70
|
+
throw new Error(`基础 setup 复制 .local 文件失败(${error.code});请检查 ${sourceRoot} 与 ${destinationRoot} 的文件类型和权限后重试。`)
|
|
71
|
+
}
|
|
72
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weotro/dx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -12,13 +12,24 @@
|
|
|
12
12
|
},
|
|
13
13
|
"homepage": "https://github.com/rangershi/dx#readme",
|
|
14
14
|
"bin": {
|
|
15
|
-
"dx": "bin/dx.js"
|
|
16
|
-
"dx-with-version-env": "bin/dx-with-version-env.js"
|
|
15
|
+
"dx": "bin/dx.js"
|
|
17
16
|
},
|
|
18
17
|
"files": [
|
|
19
|
-
"bin/",
|
|
20
|
-
"lib/",
|
|
21
|
-
"
|
|
18
|
+
"bin/dx.js",
|
|
19
|
+
"lib/logger.js",
|
|
20
|
+
"lib/version.js",
|
|
21
|
+
"lib/env.js",
|
|
22
|
+
"lib/setup.js",
|
|
23
|
+
"lib/codex-initial.js",
|
|
24
|
+
"skills",
|
|
25
|
+
"example/README.md",
|
|
26
|
+
"example/.env.development",
|
|
27
|
+
"example/dx/scripts/setup",
|
|
28
|
+
"example/dx/scripts/start",
|
|
29
|
+
"example/dx/scripts/build",
|
|
30
|
+
"example/dx/scripts/test",
|
|
31
|
+
"example/dx/scripts/db",
|
|
32
|
+
"example/dx/scripts/deploy",
|
|
22
33
|
"LICENSE",
|
|
23
34
|
"README.md",
|
|
24
35
|
"package.json"
|
|
@@ -26,13 +37,9 @@
|
|
|
26
37
|
"publishConfig": {
|
|
27
38
|
"access": "public"
|
|
28
39
|
},
|
|
29
|
-
"dependencies": {
|
|
30
|
-
"strip-json-comments": "^5.0.3",
|
|
31
|
-
"yaml": "^2.8.1"
|
|
32
|
-
},
|
|
33
40
|
"devDependencies": {
|
|
34
41
|
"jest": "^29.7.0",
|
|
35
|
-
"
|
|
42
|
+
"yaml": "^2.9.0"
|
|
36
43
|
},
|
|
37
44
|
"scripts": {
|
|
38
45
|
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
|
|
@@ -40,6 +47,13 @@
|
|
|
40
47
|
"engines": {
|
|
41
48
|
"node": ">=20.11.0"
|
|
42
49
|
},
|
|
43
|
-
"
|
|
44
|
-
"
|
|
50
|
+
"packageManager": "pnpm@10.30.0+sha512.2b5753de015d480eeb88f5b5b61e0051f05b4301808a82ec8b840c9d2adf7748eb352c83f5c1593ca703ff1017295bc3fdd3119abb9686efc96b9fcb18200937",
|
|
51
|
+
"os": [
|
|
52
|
+
"darwin",
|
|
53
|
+
"linux"
|
|
54
|
+
],
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"dotenv": "^17.4.2",
|
|
57
|
+
"dotenv-expand": "^13.0.0"
|
|
58
|
+
}
|
|
45
59
|
}
|
package/skills/ask_3th/SKILL.md
CHANGED
|
@@ -20,6 +20,8 @@ description: 显式调用 ask_3th 或被已授权流程调用时,向第三方
|
|
|
20
20
|
|
|
21
21
|
仅在上述路由选择 Claude 时使用当前技能目录的 `scripts/ask_3th.py`;它固定为咨询模式,调用细节与失败状态由 `delegate_3th` 维护。脚本自身不能启动宿主 sub-agent,收到失败状态后由执行本技能的 agent 完成回退。
|
|
22
22
|
|
|
23
|
+
在 Codex 宿主下默认不传模型参数,由 Claude Code CLI 按当前生效的用户/项目配置及环境变量选择模型。调用方不要补上固定模型 ID;返回中的 `requested_model: null` 表示使用 CLI 默认选择,实际模型未知时不要推测。
|
|
24
|
+
|
|
23
25
|
```bash
|
|
24
26
|
python3 "$HOME/.agents/skills/ask_3th/scripts/ask_3th.py" --prompt-file /绝对路径/问题.txt
|
|
25
27
|
```
|
|
@@ -42,13 +42,15 @@ python3 "$HOME/.agents/skills/delegate_3th/scripts/delegate_3th.py" --prompt-fil
|
|
|
42
42
|
python3 "$HOME/.agents/skills/delegate_3th/scripts/delegate_3th.py" --mode task --cwd /绝对路径/工作区 --prompt-file /绝对路径/任务.txt
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
默认不传 `--model`,由 Claude Code CLI 使用该次调用实际生效的用户/项目配置及环境变量选择模型;技能不硬编码模型,也不读取或覆盖模型配置。Codex 宿主下的咨询与执行均遵循此规则,用户调整 Claude Code 默认模型后,后续调用随之生效。
|
|
46
|
+
|
|
47
|
+
仅在用户明确指定本次任务模型时,使用任务模式的 `--start-model <模型 ID 或别名>`;脚本将其原样传给 Claude 的 `--model`,不限制固定模型名单,也不构成重试链。需要运行验证命令时按已有授权追加具体的 `--allow-command '仓库实际验证命令'`,可重复传入。
|
|
46
48
|
|
|
47
49
|
执行模式开放 `Read,Glob,Grep,Edit,Write,Bash`,使用 `acceptEdits`,无法获批的工具请求自动拒绝;现有权限规则继续生效,允许命令不是独立沙箱。两种模式关闭 hooks、MCP 和技能,因此任务书必须包含必要项目规则。脚本默认仅执行本地开发;完整交付等超出该边界的任务直接进入 fresh sub-agent,由其承接明确授权。
|
|
48
50
|
|
|
49
51
|
通过宿主异步工具保留会话 ID 并持续读取结果,至少每 60 秒汇报有意义的进度。`--timeout` 只控制无活动告警,不自动终止任务。脚本通过输出字节和 Git 跟踪及未忽略文件的变化观察活动;忽略目录产物需另查,观测不可用不代表停滞。
|
|
50
52
|
|
|
51
|
-
脚本 stdout 返回单个 JSON:`ok` 表示咨询已返回,`review_required` 表示任务报告待验收,`fallback_required`
|
|
53
|
+
脚本 stdout 返回单个 JSON:`ok` 表示咨询已返回,`review_required` 表示任务报告待验收,`fallback_required` 表示立即进入下文回退。`requested_model: null` 表示未覆盖 CLI 模型配置,不能据此声称实际使用了某个模型;显式指定时记录该请求值。脚本只报告失败,启动宿主 sub-agent 和等待结果是本技能执行者的职责。
|
|
52
54
|
|
|
53
55
|
### Codex 插件
|
|
54
56
|
|
|
@@ -13,9 +13,6 @@ import sys
|
|
|
13
13
|
import time
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
MODELS = ("claude-fable-5-1", "claude-opus-5")
|
|
17
|
-
|
|
18
|
-
|
|
19
16
|
def workspace_activity(cwd):
|
|
20
17
|
"""记录文件本身的变化,同一脏文件继续编辑也算活动。"""
|
|
21
18
|
if cwd is None:
|
|
@@ -83,42 +80,42 @@ def ask(prompt, timeout=120, *, mode="consult", cwd=None, start_model=None, allo
|
|
|
83
80
|
raise ValueError("代码任务必须指定存在的工作目录")
|
|
84
81
|
if mode == "consult" and (start_model is not None or allow_commands):
|
|
85
82
|
raise ValueError("咨询模式不接受任务专用参数")
|
|
86
|
-
|
|
83
|
+
if start_model is not None and (not isinstance(start_model, str) or not start_model.strip()):
|
|
84
|
+
raise ValueError("显式模型名称不能为空")
|
|
87
85
|
cli = shutil.which("claude")
|
|
88
86
|
if not cli:
|
|
89
87
|
return {"status": "fallback_required", "attempts": [], "reason": "未找到 claude 命令",
|
|
90
88
|
"next_executor": "fresh_subagent", "partial_work_possible": False}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
89
|
+
model = start_model
|
|
90
|
+
print(f"正在调用 {model or 'Claude Code 配置的默认模型'}({mode})…", file=sys.stderr, flush=True)
|
|
91
|
+
command = [
|
|
92
|
+
cli, "--print", "--output-format", "stream-json",
|
|
93
|
+
"--verbose", "--include-partial-messages",
|
|
94
|
+
"--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}',
|
|
95
|
+
"--disable-slash-commands", "--no-session-persistence",
|
|
96
|
+
"--settings", '{"disableAllHooks":true}',
|
|
97
|
+
]
|
|
98
|
+
if model is not None:
|
|
99
|
+
command += ["--model", model]
|
|
100
|
+
if mode == "consult":
|
|
101
|
+
command += ["--tools", "", "--system-prompt", "你是决策与问答顾问。根据提供的事实用简体中文回答,说明关键理由和不确定性。仅提供建议。"]
|
|
102
|
+
else:
|
|
103
|
+
command += [
|
|
104
|
+
"--tools", "Read,Glob,Grep,Edit,Write,Bash",
|
|
105
|
+
"--permission-mode", "acceptEdits", "--permission-prompts", "none",
|
|
106
|
+
"--append-system-prompt",
|
|
107
|
+
"你是协作开发者。按任务书实际修改代码并验证。你不是唯一的执行者,保留其他人的改动,遵守分配的文件范围。"
|
|
108
|
+
"先阅读适用的 AGENTS.md 和 CLAUDE.md;接续任务先检查当前状态,仅完成剩余工作。"
|
|
109
|
+
"仅执行本地开发和验证,不提交、推送、发布或对外发消息。权限被拒绝时报告具体阻塞,不尝试绕过。"
|
|
110
|
+
"完成后用简体中文报告变更文件、验证命令及结果、未完成项。",
|
|
102
111
|
]
|
|
103
|
-
if
|
|
104
|
-
command += ["--
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
"你是协作开发者。按任务书实际修改代码并验证。你不是唯一的执行者,保留其他人的改动,遵守分配的文件范围。"
|
|
111
|
-
"先阅读适用的 AGENTS.md 和 CLAUDE.md;接续任务先检查当前状态,仅完成剩余工作。"
|
|
112
|
-
"仅执行本地开发和验证,不提交、推送、发布或对外发消息。权限被拒绝时报告具体阻塞,不尝试绕过。"
|
|
113
|
-
"完成后用简体中文报告变更文件、验证命令及结果、未完成项。",
|
|
114
|
-
]
|
|
115
|
-
if allow_commands:
|
|
116
|
-
command += ["--allowedTools", *[f"Bash({item})" for item in allow_commands]]
|
|
117
|
-
try:
|
|
118
|
-
process = run_task(command, prompt, timeout, cwd)
|
|
119
|
-
except OSError as error:
|
|
120
|
-
attempts.append({"requested_model": model, "reason": f"无法运行 CLI:{type(error).__name__}"})
|
|
121
|
-
break
|
|
112
|
+
if allow_commands:
|
|
113
|
+
command += ["--allowedTools", *[f"Bash({item})" for item in allow_commands]]
|
|
114
|
+
try:
|
|
115
|
+
process = run_task(command, prompt, timeout, cwd)
|
|
116
|
+
except OSError as error:
|
|
117
|
+
reason = f"无法运行 CLI:{type(error).__name__}"
|
|
118
|
+
else:
|
|
122
119
|
try:
|
|
123
120
|
payload = {}
|
|
124
121
|
for line in process.stdout.splitlines():
|
|
@@ -136,7 +133,7 @@ def ask(prompt, timeout=120, *, mode="consult", cwd=None, start_model=None, allo
|
|
|
136
133
|
and isinstance(answer, str) and answer.strip()):
|
|
137
134
|
return {"status": "review_required" if mode == "task" else "ok", "requested_model": model,
|
|
138
135
|
"permission_denials": len(payload.get("permission_denials") or []),
|
|
139
|
-
"answer": answer.strip(), "attempts":
|
|
136
|
+
"answer": answer.strip(), "attempts": []}
|
|
140
137
|
# 原始错误可能包含账户信息,仅输出有界的分类原因。
|
|
141
138
|
diagnostic = (process.stderr + process.stdout).lower()
|
|
142
139
|
if any(word in diagnostic for word in ("quota", "credit", "usage limit", "rate_limit", "rate limit", "429")):
|
|
@@ -147,11 +144,8 @@ def ask(prompt, timeout=120, *, mode="consult", cwd=None, start_model=None, allo
|
|
|
147
144
|
reason = "模型名称无效或当前账户无权访问"
|
|
148
145
|
else:
|
|
149
146
|
reason = f"CLI 未返回有效答案(退出码 {process.returncode})"
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
"reason": attempts[-1]["reason"], "next_executor": "fresh_subagent",
|
|
153
|
-
"partial_work_possible": mode == "task"}
|
|
154
|
-
return result
|
|
147
|
+
return {"status": "fallback_required", "attempts": [{"requested_model": model, "reason": reason}],
|
|
148
|
+
"reason": reason, "next_executor": "fresh_subagent", "partial_work_possible": mode == "task"}
|
|
155
149
|
|
|
156
150
|
|
|
157
151
|
def main(argv=None):
|
|
@@ -159,7 +153,7 @@ def main(argv=None):
|
|
|
159
153
|
parser.add_argument("--prompt-file", required=True, type=Path)
|
|
160
154
|
parser.add_argument("--mode", choices=("consult", "task"), default="consult")
|
|
161
155
|
parser.add_argument("--cwd", type=Path)
|
|
162
|
-
parser.add_argument("--start-model",
|
|
156
|
+
parser.add_argument("--start-model", help="仅任务模式:显式覆盖模型;省略时使用 Claude Code 配置的默认模型")
|
|
163
157
|
parser.add_argument("--allow-command", action="append", default=[])
|
|
164
158
|
parser.add_argument("--timeout", type=float, help="无活动告警阈值(秒),不终止或切换模型")
|
|
165
159
|
args = parser.parse_args(argv)
|
|
@@ -171,6 +165,8 @@ def main(argv=None):
|
|
|
171
165
|
parser.error("任务模式必须通过 --cwd 指定存在的工作目录")
|
|
172
166
|
if args.mode == "consult" and (args.start_model or args.allow_command):
|
|
173
167
|
parser.error("--start-model 和 --allow-command 仅用于任务模式")
|
|
168
|
+
if args.start_model is not None and not args.start_model.strip():
|
|
169
|
+
parser.error("--start-model 不能为空;使用默认模型时请省略此参数")
|
|
174
170
|
try:
|
|
175
171
|
prompt = args.prompt_file.read_text(encoding="utf-8")
|
|
176
172
|
if not prompt.strip():
|
|
@@ -24,12 +24,13 @@ class AskCCTest(unittest.TestCase):
|
|
|
24
24
|
def test_cli_entrypoints_do_not_switch_models_after_idle_threshold(self):
|
|
25
25
|
skills = Path(__file__).resolve().parents[2]
|
|
26
26
|
entries = [
|
|
27
|
-
(skills / "ask_3th/scripts/ask_3th.py", "consult"),
|
|
28
|
-
(skills / "delegate_3th/scripts/delegate_3th.py", "consult"),
|
|
29
|
-
(skills / "delegate_3th/scripts/delegate_3th.py", "task"),
|
|
27
|
+
(skills / "ask_3th/scripts/ask_3th.py", "consult", None),
|
|
28
|
+
(skills / "delegate_3th/scripts/delegate_3th.py", "consult", None),
|
|
29
|
+
(skills / "delegate_3th/scripts/delegate_3th.py", "task", None),
|
|
30
|
+
(skills / "delegate_3th/scripts/delegate_3th.py", "task", "custom-model-alias"),
|
|
30
31
|
]
|
|
31
|
-
for entry, mode in entries:
|
|
32
|
-
with self.subTest(entry=entry, mode=mode), tempfile.TemporaryDirectory() as tmp:
|
|
32
|
+
for entry, mode, model in entries:
|
|
33
|
+
with self.subTest(entry=entry, mode=mode, model=model), tempfile.TemporaryDirectory() as tmp:
|
|
33
34
|
root = Path(tmp)
|
|
34
35
|
fake_cli = root / "claude"
|
|
35
36
|
fake_cli.write_text(
|
|
@@ -38,7 +39,7 @@ class AskCCTest(unittest.TestCase):
|
|
|
38
39
|
"from pathlib import Path\n"
|
|
39
40
|
"sys.stdin.read()\n"
|
|
40
41
|
"with Path(os.environ['DELEGATE_TEST_CALLS']).open('a') as log:\n"
|
|
41
|
-
" log.write(sys.argv[
|
|
42
|
+
" log.write(json.dumps({'args': sys.argv[1:], 'configured': os.environ.get('ANTHROPIC_MODEL')}) + '\\n')\n"
|
|
42
43
|
"time.sleep(0.2)\n"
|
|
43
44
|
"print(json.dumps({'type': 'result', 'subtype': 'success', 'result': 'done'}))\n"
|
|
44
45
|
)
|
|
@@ -47,16 +48,29 @@ class AskCCTest(unittest.TestCase):
|
|
|
47
48
|
prompt.write_text("test")
|
|
48
49
|
calls = root / "calls.txt"
|
|
49
50
|
env = dict(os.environ, PATH=str(root) + os.pathsep + os.environ.get("PATH", ""),
|
|
50
|
-
DELEGATE_TEST_CALLS=str(calls))
|
|
51
|
+
DELEGATE_TEST_CALLS=str(calls), ANTHROPIC_MODEL="user-configured-model")
|
|
51
52
|
command = [sys.executable, str(entry), "--prompt-file", str(prompt), "--timeout", "0.05"]
|
|
52
53
|
if mode == "task":
|
|
53
54
|
command += ["--mode", "task", "--cwd", tmp]
|
|
55
|
+
if model is not None:
|
|
56
|
+
command += ["--start-model", model]
|
|
54
57
|
completed = subprocess.run(command, env=env, capture_output=True, text=True, timeout=10)
|
|
55
58
|
self.assertEqual(completed.returncode, 0, completed.stderr)
|
|
56
59
|
response = json.loads(completed.stdout)
|
|
57
60
|
self.assertEqual(response["status"], "review_required" if mode == "task" else "ok")
|
|
58
61
|
self.assertEqual(response["attempts"], [])
|
|
59
|
-
self.assertEqual(
|
|
62
|
+
self.assertEqual(response["requested_model"], model)
|
|
63
|
+
invocations = [json.loads(line) for line in calls.read_text().splitlines()]
|
|
64
|
+
self.assertEqual(len(invocations), 1)
|
|
65
|
+
invocation = invocations[0]
|
|
66
|
+
self.assertEqual(invocation["configured"], "user-configured-model")
|
|
67
|
+
actual_args = invocation["args"]
|
|
68
|
+
if model is None:
|
|
69
|
+
self.assertNotIn("--model", actual_args)
|
|
70
|
+
else:
|
|
71
|
+
self.assertEqual(actual_args[actual_args.index("--model") + 1], model)
|
|
72
|
+
self.assertNotIn("--setting-sources", actual_args)
|
|
73
|
+
self.assertNotIn("model", json.loads(actual_args[actual_args.index("--settings") + 1]))
|
|
60
74
|
self.assertIn("疑似停滞", completed.stderr)
|
|
61
75
|
|
|
62
76
|
@patch.object(ask_3th.shutil, "which", return_value="/fake/claude")
|
|
@@ -65,6 +79,8 @@ class AskCCTest(unittest.TestCase):
|
|
|
65
79
|
response = ask_3th.ask("修改代码", mode="task", cwd=cwd, allow_commands=["python3 -m unittest"])
|
|
66
80
|
command = run.call_args.args[0]
|
|
67
81
|
self.assertEqual(response["status"], "review_required")
|
|
82
|
+
self.assertIsNone(response["requested_model"])
|
|
83
|
+
self.assertNotIn("--model", command)
|
|
68
84
|
self.assertEqual(command[command.index("--permission-mode") + 1], "acceptEdits")
|
|
69
85
|
self.assertIn("Bash(python3 -m unittest)", command)
|
|
70
86
|
self.assertNotIn("bypassPermissions", command)
|
|
@@ -87,32 +103,42 @@ class AskCCTest(unittest.TestCase):
|
|
|
87
103
|
@patch.object(ask_3th.shutil, "which", return_value="/fake/claude")
|
|
88
104
|
def test_explicit_model_failure_requests_fresh_subagent(self, _which):
|
|
89
105
|
with tempfile.TemporaryDirectory() as cwd, patch.object(ask_3th, "run_task", return_value=result("usage limit", True)):
|
|
90
|
-
response = ask_3th.ask("接续", mode="task", cwd=cwd, start_model="
|
|
106
|
+
response = ask_3th.ask("接续", mode="task", cwd=cwd, start_model="custom-model-alias")
|
|
91
107
|
self.assertEqual(response["status"], "fallback_required")
|
|
92
108
|
self.assertEqual(response["next_executor"], "fresh_subagent")
|
|
93
109
|
self.assertNotIn("next_model", response)
|
|
110
|
+
self.assertEqual(response["attempts"][0]["requested_model"], "custom-model-alias")
|
|
94
111
|
|
|
95
112
|
def test_task_requires_explicit_directory(self):
|
|
96
113
|
with self.assertRaises(ValueError):
|
|
97
114
|
ask_3th.ask("修改代码", mode="task")
|
|
98
115
|
|
|
116
|
+
def test_empty_model_is_rejected_instead_of_overriding_cli_default(self):
|
|
117
|
+
with tempfile.TemporaryDirectory() as cwd:
|
|
118
|
+
for model in ["", " "]:
|
|
119
|
+
with self.subTest(model=model), self.assertRaises(ValueError):
|
|
120
|
+
ask_3th.ask("修改代码", mode="task", cwd=cwd, start_model=model)
|
|
121
|
+
completed = subprocess.run([
|
|
122
|
+
sys.executable, str(Path(ask_3th.__file__)), "--mode", "task", "--cwd", cwd,
|
|
123
|
+
"--prompt-file", str(Path(cwd, "unused.txt")), "--start-model=",
|
|
124
|
+
], capture_output=True, text=True, timeout=10)
|
|
125
|
+
self.assertEqual(completed.returncode, 2)
|
|
126
|
+
self.assertIn("--start-model 不能为空", completed.stderr)
|
|
127
|
+
|
|
99
128
|
@patch.object(ask_3th.shutil, "which", return_value="/fake/claude")
|
|
100
129
|
def test_first_failure_requests_fallback_without_second_model(self, _which):
|
|
101
130
|
seen = []
|
|
102
131
|
|
|
103
132
|
def cli(command, *args):
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if model == "claude-fable-5-1":
|
|
107
|
-
return result("usage limit exceeded", True)
|
|
108
|
-
if model == "claude-opus-5":
|
|
109
|
-
return result("OK")
|
|
110
|
-
return result(f"There's an issue with the selected model ({model}).", True)
|
|
133
|
+
seen.append(command)
|
|
134
|
+
return result("usage limit exceeded", True)
|
|
111
135
|
|
|
112
136
|
with patch.object(ask_3th, "run_task", side_effect=cli):
|
|
113
137
|
response = ask_3th.ask("仅回答 OK")
|
|
114
138
|
self.assertEqual(response["status"], "fallback_required")
|
|
115
|
-
self.assertEqual(seen,
|
|
139
|
+
self.assertEqual(len(seen), 1)
|
|
140
|
+
self.assertNotIn("--model", seen[0])
|
|
141
|
+
self.assertIsNone(response["attempts"][0]["requested_model"])
|
|
116
142
|
self.assertEqual(response["next_executor"], "fresh_subagent")
|
|
117
143
|
self.assertEqual(response["reason"], "额度不足或限流")
|
|
118
144
|
|