@casatwy/deyo 0.2.0 → 0.2.3
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 +13 -4
- package/deyo.mjs +8 -6
- package/openclaw-skill-update.mjs +794 -0
- package/package.json +3 -2
- package/skill-manager.mjs +123 -26
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`deyo` 是 [https://deyo.miaobi.fun](https://deyo.miaobi.fun) 的命令行工具。你可以直接在终端里提交音视频链接或本地音视频文件进行转写,并把结果输出到终端或保存到本地文件。
|
|
4
4
|
|
|
5
|
-
CLI `0.2.
|
|
5
|
+
CLI `0.2.2` 保留省略 `--language` 时的自动检测和可选稳定正文事件,并新增由 npm CLI 管理的 OpenClaw 更新登记、离线状态与显式确认流程。默认不会把正文写入进度日志;只有显式使用 `--stream-transcript --progress-format jsonl`,且最终格式为 `text` 时,才会在 stderr JSONL 中输出稳定的 Whisper 正文增量。stdout 或 `-O` 始终只接收最终 Whisper 原稿,CLI 本身不做 AI 整理。
|
|
6
6
|
|
|
7
7
|
当前支持 8 类链接来源 + 本地音视频上传。链接来源包括小宇宙单集、喜马拉雅单集(含 `xima.tv` 短链)、B 站单视频、抖音单视频、小红书单视频笔记、YouTube 单视频、Apple Podcasts 单集和 Twitter/X 单条推文。其中 YouTube 如果命中可直接使用的字幕,会直接输出字幕或文本结果,不进入完整转写、不扣分钟;喜马拉雅合集链接会返回 422 并提示选择单集,不会扣分钟或创建转写任务;抖音图文内容、小红书图文笔记、Twitter/X 文本或图片推文会返回不支持转写的提示。B 站支持普通 BV 链接、`b23.tv` 短链、带 `bvid` 的 `player.bilibili.com/player.html` 嵌入播放器链接,以及可由 Whisper 解出 BV 页面的 `bilibili://video/...` App 分享视频链接;CLI 会识别并透传 App 链接,不在本地解码或推导 BV。
|
|
8
8
|
|
|
@@ -28,7 +28,7 @@ deyo --help
|
|
|
28
28
|
|
|
29
29
|
## 安装 Deyo Skill
|
|
30
30
|
|
|
31
|
-
Deyo CLI 和 Deyo Skill 是两个独立产物:CLI 负责登录、上传、转写、结构化进度和 Whisper 原稿;Skill 负责让 Codex、Claude、Gemini 或 OpenClaw 正确调用 CLI,并在普通 text
|
|
31
|
+
Deyo CLI 和 Deyo Skill 是两个独立产物:CLI 负责登录、上传、转写、结构化进度和 Whisper 原稿;Skill 负责让 Codex、Claude、Gemini 或 OpenClaw 正确调用 CLI,并在普通 text 场景按约束整理正文。它们独立安装、独立版本、独立更新,版本号不要求相同。Skill `1.0.11` 要求 CLI 至少为 `0.2.2`。
|
|
32
32
|
|
|
33
33
|
先确认 CLI 版本,再交给 Deyo CLI 调用各平台的原生管理器安装 Skill:
|
|
34
34
|
|
|
@@ -47,7 +47,7 @@ deyo skill install --platform openclaw
|
|
|
47
47
|
- Codex:添加 `casatwy/deyo-skill` Git marketplace 并安装 `deyo@deyo-official`;新会话生效,后续由宿主刷新 marketplace。
|
|
48
48
|
- Claude:合并同名官方 marketplace 配置、设置 `autoUpdate: true`,再安装 `deyo@deyo-official`;运行 `/reload-plugins` 或开始新会话后生效。
|
|
49
49
|
- Gemini:从 Deyo 官方 Git 仓库安装 extension 并启用 `--auto-update`;不会添加固定 `--ref` 或跳过来源确认的 `--consent`,更新后重启 Gemini CLI。
|
|
50
|
-
- OpenClaw:使用 owner-qualified 的 `@casatwy/deyo
|
|
50
|
+
- OpenClaw:使用 owner-qualified 的 `@casatwy/deyo`;fresh install 成功后严格回读官方 managed origin 并登记调用时检查,已有官方 managed 安装只登记、不在安装命令中更新。直接运行 OpenClaw 原生命令安装的副本不会被自动登记。
|
|
51
51
|
|
|
52
52
|
离线查看本地安装状态:
|
|
53
53
|
|
|
@@ -59,6 +59,15 @@ deyo skill status --platform claude --json
|
|
|
59
59
|
|
|
60
60
|
`status` 只读取本地设置、安装 metadata 和 manifest;它不会启动平台 CLI、访问网络或触发更新。
|
|
61
61
|
|
|
62
|
+
OpenClaw 的调用时检查默认每 24 小时至多验证一次 owner-qualified `latest` 候选,只接受稳定版本、`resolvedFrom: tag`、`tag: latest` 和 security `pass/clean`,不会直接安装。发现候选后,Skill 会先展示候选、来源和安全结果并询问 `是否现在更新Deyo到latest`。只有本轮得到明确同意后,才执行:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
deyo skill update --platform openclaw --scope global \
|
|
66
|
+
--confirm-latest 'update @casatwy/deyo to latest now'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
workspace 登记必须把 scope 改为 `workspace`。更新前 CLI 会重新验证 `latest`;候选变化时旧确认立即失效并要求重新确认。成功、origin 变化或结果不确定时要求重新调用 `/deyo`,已知失败且 origin 未变化时继续使用旧版。所有原生管理器输出都与转写 stdout、JSONL、raw 和 cleaned 文件隔离;流程不会使用 `--all`、`--force`、`--force-install` 或风险确认绕过,也不会创建 Cron。设置 `DEYO_OPENCLAW_AUTO_UPDATE=0` 可退出调用时检查。
|
|
70
|
+
|
|
62
71
|
如果检测到旧版手工复制的 `deyo` Skill 目录,安装会停止并显示完整目录树 SHA-256。只有目录路径、文件、内容、类型、模式和 symlink 都与已知官方历史版本完全一致时,才允许把该摘要原样传回:
|
|
63
72
|
|
|
64
73
|
```bash
|
|
@@ -246,7 +255,7 @@ deyo --api-key 'deyo_sk_other' 'https://www.youtube.com/watch?v=xxxx'
|
|
|
246
255
|
|
|
247
256
|
`--language <value>`
|
|
248
257
|
|
|
249
|
-
|
|
258
|
+
显式指定转写语言,例如 `zh` 或 `en`。省略该参数时,CLI 不会在链接任务或本地文件任务的请求体里发送 `language` 字段,由服务端和 Whisper 自动检测;不要根据终端语言或内容标题自行推断。
|
|
250
259
|
|
|
251
260
|
`-O, --output <path>`
|
|
252
261
|
|
package/deyo.mjs
CHANGED
|
@@ -11,7 +11,6 @@ import { handleSkillCommand } from './skill-manager.mjs'
|
|
|
11
11
|
const CLI_VERSION = createRequire(import.meta.url)('./package.json').version
|
|
12
12
|
|
|
13
13
|
const DEFAULT_BASE_URL = 'https://deyo.miaobi.fun'
|
|
14
|
-
const DEFAULT_LANGUAGE = 'zh'
|
|
15
14
|
const POLL_INTERVAL_MS = 1500
|
|
16
15
|
const SSE_RECONNECT_DELAYS_MS = [1000, 2000, 4000]
|
|
17
16
|
const SSE_RECOVERY_INTERVAL_MS = 15000
|
|
@@ -83,7 +82,7 @@ function printUsage() {
|
|
|
83
82
|
deyo [选项] <url|file>
|
|
84
83
|
deyo [选项] --file <file>
|
|
85
84
|
deyo auth <login|logout|status> [选项]
|
|
86
|
-
deyo skill <install|status> [选项]
|
|
85
|
+
deyo skill <install|status|update> [选项]
|
|
87
86
|
|
|
88
87
|
转写选项:
|
|
89
88
|
--api-key <key> 可选,Deyo API key
|
|
@@ -91,7 +90,7 @@ function printUsage() {
|
|
|
91
90
|
--source <name> 可选,显式指定来源:xiaoyuzhou | ximalaya | bilibili | douyin | xiaohongshu | youtube | apple-podcasts | twitter
|
|
92
91
|
--file <path> 可选,显式指定本地单个音视频文件
|
|
93
92
|
--mime-type <type> 可选,上传本地文件时显式指定 MIME,例如 audio/mpeg 或 video/mp4
|
|
94
|
-
--language <value>
|
|
93
|
+
--language <value> 可选,显式指定转写语言;省略时自动检测
|
|
95
94
|
--format <value> 可选,text | srt | vtt | json | verbose_json
|
|
96
95
|
--progress-format <v> 可选,auto | text | jsonl,默认 auto
|
|
97
96
|
--stream-transcript 可选,通过 stderr JSONL 输出稳定正文;仅支持 text 且必须搭配 --progress-format jsonl
|
|
@@ -107,6 +106,8 @@ auth 子命令:
|
|
|
107
106
|
Skill 管理:
|
|
108
107
|
deyo skill install --platform codex|claude|gemini|openclaw [--scope user|workspace|global]
|
|
109
108
|
deyo skill status [--platform codex|claude|gemini|openclaw] [--json]
|
|
109
|
+
deyo skill update --platform openclaw [--scope workspace|global]
|
|
110
|
+
--confirm-latest 'update @casatwy/deyo to latest now'
|
|
110
111
|
|
|
111
112
|
API key 读取优先级:
|
|
112
113
|
1. --api-key
|
|
@@ -2085,7 +2086,7 @@ async function uploadLocalFile(fileInput, args, baseUrl, apiKey, reporter, runCo
|
|
|
2085
2086
|
retry: true,
|
|
2086
2087
|
body: {
|
|
2087
2088
|
uploadId,
|
|
2088
|
-
language: args.language
|
|
2089
|
+
...(args.language ? { language: args.language } : {}),
|
|
2089
2090
|
},
|
|
2090
2091
|
})
|
|
2091
2092
|
const checkedTask = getUploadTaskFromCheckResult(checkResult)
|
|
@@ -2192,7 +2193,7 @@ async function handleAuthCommand(argv) {
|
|
|
2192
2193
|
function buildUrlTaskCreatePayload(args, input) {
|
|
2193
2194
|
const createPayload = {
|
|
2194
2195
|
url: input.url,
|
|
2195
|
-
language: args.language
|
|
2196
|
+
...(args.language ? { language: args.language } : {}),
|
|
2196
2197
|
}
|
|
2197
2198
|
|
|
2198
2199
|
if (args.source) {
|
|
@@ -2529,7 +2530,8 @@ async function main() {
|
|
|
2529
2530
|
}
|
|
2530
2531
|
|
|
2531
2532
|
if (primaryCommand === 'skill') {
|
|
2532
|
-
await handleSkillCommand(argv, CLI_VERSION)
|
|
2533
|
+
const exitCode = await handleSkillCommand(argv, CLI_VERSION)
|
|
2534
|
+
if (Number.isSafeInteger(exitCode)) process.exitCode = exitCode
|
|
2533
2535
|
return
|
|
2534
2536
|
}
|
|
2535
2537
|
|
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
import { constants as fsConstants } from 'node:fs'
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import {
|
|
5
|
+
chmod,
|
|
6
|
+
lstat,
|
|
7
|
+
mkdir,
|
|
8
|
+
open,
|
|
9
|
+
readFile,
|
|
10
|
+
rename,
|
|
11
|
+
rm,
|
|
12
|
+
} from 'node:fs/promises'
|
|
13
|
+
import os from 'node:os'
|
|
14
|
+
import path from 'node:path'
|
|
15
|
+
|
|
16
|
+
const DAY_MS = 24 * 60 * 60 * 1000
|
|
17
|
+
const TIMEOUT_MS = 20_000
|
|
18
|
+
const OWNER = 'casatwy'
|
|
19
|
+
const SLUG = 'deyo'
|
|
20
|
+
const REGISTRY = 'https://clawhub.ai'
|
|
21
|
+
const OWNER_QUALIFIED_SKILL = `@${OWNER}/${SLUG}`
|
|
22
|
+
export const CONFIRM_LATEST_PHRASE = `update ${OWNER_QUALIFIED_SKILL} to latest now`
|
|
23
|
+
const UPDATED_EXIT_CODE = 10
|
|
24
|
+
const CONFIRMATION_EXIT_CODE = 11
|
|
25
|
+
const STABLE_SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/
|
|
26
|
+
const PRIVATE_DIRECTORY_MODE = 0o700
|
|
27
|
+
const PRIVATE_FILE_MODE = 0o600
|
|
28
|
+
|
|
29
|
+
function normalizeOwner(value) {
|
|
30
|
+
return typeof value === 'string' ? value.trim().replace(/^@/, '').toLowerCase() : ''
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeSlug(value) {
|
|
34
|
+
if (typeof value !== 'string') return ''
|
|
35
|
+
return value.trim().replace(/^@casatwy\//i, '').toLowerCase()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function compareSemver(left, right) {
|
|
39
|
+
if (!STABLE_SEMVER.test(left) || !STABLE_SEMVER.test(right)) return null
|
|
40
|
+
const a = left.split('.').map(Number)
|
|
41
|
+
const b = right.split('.').map(Number)
|
|
42
|
+
for (let index = 0; index < 3; index += 1) {
|
|
43
|
+
if (a[index] !== b[index]) return a[index] - b[index]
|
|
44
|
+
}
|
|
45
|
+
return 0
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function absoluteConfiguredPath(name, value, fallback) {
|
|
49
|
+
const selected = String(value || fallback || '').trim()
|
|
50
|
+
if (!selected || !path.isAbsolute(selected)) {
|
|
51
|
+
throw new Error(`${name} must be an absolute path`)
|
|
52
|
+
}
|
|
53
|
+
return path.normalize(selected)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function homeDirectory(environment) {
|
|
57
|
+
return absoluteConfiguredPath(
|
|
58
|
+
'HOME',
|
|
59
|
+
environment.HOME || environment.USERPROFILE,
|
|
60
|
+
os.homedir(),
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function openClawStateDirectory(environment) {
|
|
65
|
+
const home = homeDirectory(environment)
|
|
66
|
+
if (environment.OPENCLAW_STATE_DIR) {
|
|
67
|
+
return absoluteConfiguredPath('OPENCLAW_STATE_DIR', environment.OPENCLAW_STATE_DIR)
|
|
68
|
+
}
|
|
69
|
+
const openClawHome = absoluteConfiguredPath('OPENCLAW_HOME', environment.OPENCLAW_HOME, home)
|
|
70
|
+
return path.join(openClawHome, '.openclaw')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function configRoot(environment) {
|
|
74
|
+
return absoluteConfiguredPath(
|
|
75
|
+
'XDG_CONFIG_HOME',
|
|
76
|
+
environment.XDG_CONFIG_HOME,
|
|
77
|
+
path.join(homeDirectory(environment), '.config'),
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function stateRoot(environment) {
|
|
82
|
+
return absoluteConfiguredPath(
|
|
83
|
+
'XDG_STATE_HOME',
|
|
84
|
+
environment.XDG_STATE_HOME,
|
|
85
|
+
path.join(homeDirectory(environment), '.local', 'state'),
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function resolveManagedOriginPath({ scope, cwd = process.cwd(), env = process.env }) {
|
|
90
|
+
if (scope === 'workspace') {
|
|
91
|
+
return path.resolve(cwd, 'skills', SLUG, '.clawhub', 'origin.json')
|
|
92
|
+
}
|
|
93
|
+
if (scope !== 'global') throw new Error('OpenClaw scope must be global or workspace')
|
|
94
|
+
return path.join(openClawStateDirectory(env), 'skills', SLUG, '.clawhub', 'origin.json')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function installIdentity(scope, originPath) {
|
|
98
|
+
return createHash('sha256')
|
|
99
|
+
.update(`${scope}\0${path.normalize(originPath)}`, 'utf8')
|
|
100
|
+
.digest('hex')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function resolveOpenClawUpdatePaths({ scope, cwd = process.cwd(), env = process.env }) {
|
|
104
|
+
const originPath = resolveManagedOriginPath({ scope, cwd, env })
|
|
105
|
+
const identity = installIdentity(scope, originPath)
|
|
106
|
+
const enrollmentDirectory = path.join(configRoot(env), 'deyo', 'openclaw-skill-update')
|
|
107
|
+
const runtimeDirectory = path.join(stateRoot(env), 'deyo', 'openclaw-skill-update')
|
|
108
|
+
return {
|
|
109
|
+
identity,
|
|
110
|
+
originPath,
|
|
111
|
+
enrollmentDirectory,
|
|
112
|
+
enrollmentPath: path.join(enrollmentDirectory, `${identity}.json`),
|
|
113
|
+
runtimeDirectory,
|
|
114
|
+
runtimePath: path.join(runtimeDirectory, `${identity}.json`),
|
|
115
|
+
lockPath: path.join(runtimeDirectory, `${identity}.lock`),
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function pathLstat(target) {
|
|
120
|
+
try {
|
|
121
|
+
return await lstat(target)
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error?.code === 'ENOENT') return null
|
|
124
|
+
throw error
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function inspectPrivateJson(target) {
|
|
129
|
+
const directory = path.dirname(target)
|
|
130
|
+
const ownedRoot = path.dirname(directory)
|
|
131
|
+
const [directoryStats, ownedRootStats] = await Promise.all([
|
|
132
|
+
pathLstat(directory),
|
|
133
|
+
pathLstat(ownedRoot),
|
|
134
|
+
])
|
|
135
|
+
if (!directoryStats) return { exists: false, safe: true, value: null }
|
|
136
|
+
if (
|
|
137
|
+
path.basename(ownedRoot) !== 'deyo'
|
|
138
|
+
|| !ownedRootStats
|
|
139
|
+
|| ownedRootStats.isSymbolicLink()
|
|
140
|
+
|| !ownedRootStats.isDirectory()
|
|
141
|
+
|| (ownedRootStats.mode & 0o077) !== 0
|
|
142
|
+
||
|
|
143
|
+
directoryStats.isSymbolicLink()
|
|
144
|
+
|| !directoryStats.isDirectory()
|
|
145
|
+
|| (directoryStats.mode & 0o077) !== 0
|
|
146
|
+
) {
|
|
147
|
+
return { exists: false, safe: false, value: null }
|
|
148
|
+
}
|
|
149
|
+
const stats = await pathLstat(target)
|
|
150
|
+
if (!stats) return { exists: false, safe: true, value: null }
|
|
151
|
+
if (stats.isSymbolicLink() || !stats.isFile() || (stats.mode & 0o077) !== 0) {
|
|
152
|
+
return { exists: true, safe: false, value: null }
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
return { exists: true, safe: true, value: JSON.parse(await readFile(target, 'utf8')) }
|
|
156
|
+
} catch {
|
|
157
|
+
return { exists: true, safe: false, value: null }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function ensurePrivateDirectory(target) {
|
|
162
|
+
const ownedParent = path.dirname(target)
|
|
163
|
+
const parentStats = await pathLstat(ownedParent)
|
|
164
|
+
if (
|
|
165
|
+
path.basename(ownedParent) === 'deyo'
|
|
166
|
+
&& (parentStats?.isSymbolicLink() || (parentStats && !parentStats.isDirectory()))
|
|
167
|
+
) {
|
|
168
|
+
throw new Error(`Unsafe private directory: ${ownedParent}`)
|
|
169
|
+
}
|
|
170
|
+
const existing = await pathLstat(target)
|
|
171
|
+
if (existing?.isSymbolicLink() || (existing && !existing.isDirectory())) {
|
|
172
|
+
throw new Error(`Unsafe private directory: ${target}`)
|
|
173
|
+
}
|
|
174
|
+
await mkdir(target, { recursive: true, mode: PRIVATE_DIRECTORY_MODE })
|
|
175
|
+
const createdParent = await lstat(ownedParent)
|
|
176
|
+
if (
|
|
177
|
+
path.basename(ownedParent) === 'deyo'
|
|
178
|
+
&& (createdParent.isSymbolicLink() || !createdParent.isDirectory())
|
|
179
|
+
) {
|
|
180
|
+
throw new Error(`Unsafe private directory: ${ownedParent}`)
|
|
181
|
+
}
|
|
182
|
+
if (path.basename(ownedParent) === 'deyo') await chmod(ownedParent, PRIVATE_DIRECTORY_MODE)
|
|
183
|
+
const created = await lstat(target)
|
|
184
|
+
if (created.isSymbolicLink() || !created.isDirectory()) {
|
|
185
|
+
throw new Error(`Unsafe private directory: ${target}`)
|
|
186
|
+
}
|
|
187
|
+
await chmod(target, PRIVATE_DIRECTORY_MODE)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function syncDirectory(target) {
|
|
191
|
+
let handle
|
|
192
|
+
try {
|
|
193
|
+
handle = await open(target, 'r')
|
|
194
|
+
await handle.sync()
|
|
195
|
+
} catch {
|
|
196
|
+
// Directory fsync is not available on every supported platform.
|
|
197
|
+
} finally {
|
|
198
|
+
await handle?.close().catch(() => {})
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function writePrivateJson(target, value) {
|
|
203
|
+
const parent = path.dirname(target)
|
|
204
|
+
await ensurePrivateDirectory(parent)
|
|
205
|
+
const existing = await pathLstat(target)
|
|
206
|
+
if (existing?.isSymbolicLink() || (existing && !existing.isFile())) {
|
|
207
|
+
throw new Error(`Unsafe private state file: ${target}`)
|
|
208
|
+
}
|
|
209
|
+
const temporary = path.join(
|
|
210
|
+
parent,
|
|
211
|
+
`.${path.basename(target)}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`,
|
|
212
|
+
)
|
|
213
|
+
const handle = await open(temporary, 'wx', PRIVATE_FILE_MODE)
|
|
214
|
+
let temporaryExists = true
|
|
215
|
+
try {
|
|
216
|
+
try {
|
|
217
|
+
await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, 'utf8')
|
|
218
|
+
await handle.sync()
|
|
219
|
+
} finally {
|
|
220
|
+
await handle.close()
|
|
221
|
+
}
|
|
222
|
+
await chmod(temporary, PRIVATE_FILE_MODE)
|
|
223
|
+
const rechecked = await pathLstat(target)
|
|
224
|
+
if (rechecked?.isSymbolicLink() || (rechecked && !rechecked.isFile())) {
|
|
225
|
+
throw new Error(`Unsafe private state file: ${target}`)
|
|
226
|
+
}
|
|
227
|
+
await rename(temporary, target)
|
|
228
|
+
temporaryExists = false
|
|
229
|
+
await chmod(target, PRIVATE_FILE_MODE)
|
|
230
|
+
await syncDirectory(parent)
|
|
231
|
+
} finally {
|
|
232
|
+
if (temporaryExists) await rm(temporary, { force: true })
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function acquireLock(lockPath) {
|
|
237
|
+
await ensurePrivateDirectory(path.dirname(lockPath))
|
|
238
|
+
try {
|
|
239
|
+
const handle = await open(
|
|
240
|
+
lockPath,
|
|
241
|
+
fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY,
|
|
242
|
+
PRIVATE_FILE_MODE,
|
|
243
|
+
)
|
|
244
|
+
await handle.writeFile(`${process.pid}\n`)
|
|
245
|
+
await handle.sync()
|
|
246
|
+
await chmod(lockPath, PRIVATE_FILE_MODE)
|
|
247
|
+
return handle
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (error?.code === 'EEXIST') return null
|
|
250
|
+
throw error
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function validOrigin(raw, originPath) {
|
|
255
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null
|
|
256
|
+
const registry = typeof raw.registry === 'string' ? raw.registry.replace(/\/+$/, '') : ''
|
|
257
|
+
const ownerHandle = normalizeOwner(raw.ownerHandle)
|
|
258
|
+
const slug = normalizeSlug(raw.slug)
|
|
259
|
+
const version = raw.installedVersion
|
|
260
|
+
const fingerprint = raw.fingerprint
|
|
261
|
+
if (
|
|
262
|
+
raw.version !== 1
|
|
263
|
+
|| registry !== REGISTRY
|
|
264
|
+
|| ownerHandle !== OWNER
|
|
265
|
+
|| slug !== SLUG
|
|
266
|
+
|| typeof version !== 'string'
|
|
267
|
+
|| !STABLE_SEMVER.test(version)
|
|
268
|
+
|| !Number.isFinite(raw.installedAt)
|
|
269
|
+
|| !/^[a-f0-9]{64}$/.test(fingerprint || '')
|
|
270
|
+
) return null
|
|
271
|
+
return { originPath, registry, ownerHandle, slug, version, fingerprint }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export async function readManagedOrigin(originPath) {
|
|
275
|
+
try {
|
|
276
|
+
const skillDirectory = path.dirname(path.dirname(originPath))
|
|
277
|
+
const metadataDirectory = path.dirname(originPath)
|
|
278
|
+
const [skillStats, metadataStats, originStats] = await Promise.all([
|
|
279
|
+
lstat(skillDirectory),
|
|
280
|
+
lstat(metadataDirectory),
|
|
281
|
+
lstat(originPath),
|
|
282
|
+
])
|
|
283
|
+
if (
|
|
284
|
+
skillStats.isSymbolicLink()
|
|
285
|
+
|| !skillStats.isDirectory()
|
|
286
|
+
|| metadataStats.isSymbolicLink()
|
|
287
|
+
|| !metadataStats.isDirectory()
|
|
288
|
+
|| originStats.isSymbolicLink()
|
|
289
|
+
|| !originStats.isFile()
|
|
290
|
+
) return { ok: false, reason: 'unsafe_origin', path: originPath }
|
|
291
|
+
const origin = validOrigin(JSON.parse(await readFile(originPath, 'utf8')), originPath)
|
|
292
|
+
return origin
|
|
293
|
+
? { ok: true, ...origin }
|
|
294
|
+
: { ok: false, reason: 'invalid_origin', path: originPath }
|
|
295
|
+
} catch (error) {
|
|
296
|
+
return {
|
|
297
|
+
ok: false,
|
|
298
|
+
reason: error?.code === 'ENOENT' ? 'missing_origin' : 'unreadable_origin',
|
|
299
|
+
path: originPath,
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function validEnrollment(value, { scope, identity, originPath }) {
|
|
305
|
+
return Boolean(
|
|
306
|
+
value
|
|
307
|
+
&& value.schema === 1
|
|
308
|
+
&& value.enabled === true
|
|
309
|
+
&& value.scope === scope
|
|
310
|
+
&& value.identity === identity
|
|
311
|
+
&& value.originPath === originPath
|
|
312
|
+
&& value.registry === REGISTRY
|
|
313
|
+
&& normalizeOwner(value.ownerHandle) === OWNER
|
|
314
|
+
&& normalizeSlug(value.slug) === SLUG
|
|
315
|
+
&& typeof value.enrolledAt === 'string'
|
|
316
|
+
&& typeof value.enrolledByCliVersion === 'string'
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function validRuntime(value, identity) {
|
|
321
|
+
return Boolean(
|
|
322
|
+
value
|
|
323
|
+
&& value.schema === 3
|
|
324
|
+
&& value.identity === identity
|
|
325
|
+
&& Number.isSafeInteger(value.attempt)
|
|
326
|
+
&& value.attempt >= 0
|
|
327
|
+
&& (value.lastAttemptAt === null || typeof value.lastAttemptAt === 'string')
|
|
328
|
+
&& typeof value.status === 'string'
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function isDue(state, now = Date.now()) {
|
|
333
|
+
const lastAttempt = Date.parse(state?.lastAttemptAt ?? '')
|
|
334
|
+
return !Number.isFinite(lastAttempt) || now - lastAttempt >= DAY_MS
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function isAutoUpdateDisabled(environment = process.env) {
|
|
338
|
+
return environment.DEYO_OPENCLAW_AUTO_UPDATE === '0'
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function isVerificationClean(envelope, expectedInstalledVersion, selector = 'tag') {
|
|
342
|
+
const candidateVersion = envelope?.version
|
|
343
|
+
const installedVersion = envelope?.openclaw?.resolution?.installedVersion
|
|
344
|
+
const selectorMatches = selector === 'tag'
|
|
345
|
+
? envelope?.resolvedFrom === 'tag' && envelope?.tag === 'latest'
|
|
346
|
+
: false
|
|
347
|
+
return envelope?.schema === 'clawhub.skill.verify.v1'
|
|
348
|
+
&& normalizeOwner(envelope?.publisherHandle) === OWNER
|
|
349
|
+
&& normalizeSlug(envelope?.slug) === SLUG
|
|
350
|
+
&& typeof candidateVersion === 'string'
|
|
351
|
+
&& STABLE_SEMVER.test(candidateVersion)
|
|
352
|
+
&& selectorMatches
|
|
353
|
+
&& envelope?.openclaw?.resolution?.source === 'installed'
|
|
354
|
+
&& envelope?.openclaw?.resolution?.selector === selector
|
|
355
|
+
&& installedVersion === expectedInstalledVersion
|
|
356
|
+
&& compareSemver(candidateVersion, expectedInstalledVersion) >= 0
|
|
357
|
+
&& envelope?.ok === true
|
|
358
|
+
&& envelope?.decision === 'pass'
|
|
359
|
+
&& envelope?.security?.passed === true
|
|
360
|
+
&& envelope?.security?.status === 'clean'
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export async function runWithDeadline(command, args, options = {}) {
|
|
364
|
+
const remainingMs = options.deadline - Date.now()
|
|
365
|
+
if (remainingMs <= 0) throw new Error('OpenClaw operation timed out')
|
|
366
|
+
return await new Promise((resolve, reject) => {
|
|
367
|
+
const child = spawn(command, args, {
|
|
368
|
+
cwd: options.cwd,
|
|
369
|
+
env: options.env,
|
|
370
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
371
|
+
})
|
|
372
|
+
const stdout = []
|
|
373
|
+
const stderr = []
|
|
374
|
+
let timedOut = false
|
|
375
|
+
const timer = setTimeout(() => {
|
|
376
|
+
timedOut = true
|
|
377
|
+
child.kill('SIGKILL')
|
|
378
|
+
}, remainingMs)
|
|
379
|
+
child.stdout.on('data', chunk => stdout.push(Buffer.from(chunk)))
|
|
380
|
+
child.stderr.on('data', chunk => stderr.push(Buffer.from(chunk)))
|
|
381
|
+
child.on('error', (error) => {
|
|
382
|
+
clearTimeout(timer)
|
|
383
|
+
reject(error)
|
|
384
|
+
})
|
|
385
|
+
child.on('close', (code, signal) => {
|
|
386
|
+
clearTimeout(timer)
|
|
387
|
+
if (timedOut) {
|
|
388
|
+
reject(new Error('OpenClaw operation timed out'))
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
resolve({
|
|
392
|
+
code: code ?? 1,
|
|
393
|
+
signal: signal || null,
|
|
394
|
+
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
395
|
+
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function baseRuntime(identity, attempt, attemptedAt, origin, status) {
|
|
402
|
+
return {
|
|
403
|
+
schema: 3,
|
|
404
|
+
identity,
|
|
405
|
+
lastAttemptAt: attemptedAt,
|
|
406
|
+
attempt,
|
|
407
|
+
status,
|
|
408
|
+
fromVersion: origin?.version ?? null,
|
|
409
|
+
fromFingerprint: origin?.fingerprint ?? null,
|
|
410
|
+
candidateVersion: null,
|
|
411
|
+
candidateVerifiedAt: null,
|
|
412
|
+
verificationDecision: null,
|
|
413
|
+
securityStatus: null,
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function pendingRuntime(base, candidateVersion, verifiedAt) {
|
|
418
|
+
return {
|
|
419
|
+
...base,
|
|
420
|
+
status: 'pending_confirmation',
|
|
421
|
+
candidateVersion,
|
|
422
|
+
candidateVerifiedAt: verifiedAt,
|
|
423
|
+
verificationDecision: 'pass',
|
|
424
|
+
securityStatus: 'clean',
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function readEnrollmentAndRuntime(paths, scope) {
|
|
429
|
+
const [enrollmentMetadata, runtimeMetadata] = await Promise.all([
|
|
430
|
+
inspectPrivateJson(paths.enrollmentPath),
|
|
431
|
+
inspectPrivateJson(paths.runtimePath),
|
|
432
|
+
])
|
|
433
|
+
const enrollmentState = !enrollmentMetadata.exists
|
|
434
|
+
? 'not_enrolled'
|
|
435
|
+
: enrollmentMetadata.safe && validEnrollment(enrollmentMetadata.value, {
|
|
436
|
+
scope,
|
|
437
|
+
identity: paths.identity,
|
|
438
|
+
originPath: paths.originPath,
|
|
439
|
+
})
|
|
440
|
+
? 'enrolled'
|
|
441
|
+
: 'invalid'
|
|
442
|
+
const runtimeState = !runtimeMetadata.exists
|
|
443
|
+
? 'missing'
|
|
444
|
+
: runtimeMetadata.safe && validRuntime(runtimeMetadata.value, paths.identity)
|
|
445
|
+
? 'valid'
|
|
446
|
+
: 'invalid'
|
|
447
|
+
return {
|
|
448
|
+
enrollmentState,
|
|
449
|
+
enrollment: enrollmentState === 'enrolled' ? enrollmentMetadata.value : null,
|
|
450
|
+
runtimeState,
|
|
451
|
+
runtime: runtimeState === 'valid' ? runtimeMetadata.value : null,
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export async function enrollOpenClawAutoUpdate({
|
|
456
|
+
scope,
|
|
457
|
+
cliVersion,
|
|
458
|
+
cwd = process.cwd(),
|
|
459
|
+
env = process.env,
|
|
460
|
+
now = Date.now(),
|
|
461
|
+
}) {
|
|
462
|
+
process.umask(0o077)
|
|
463
|
+
const paths = resolveOpenClawUpdatePaths({ scope, cwd, env })
|
|
464
|
+
const origin = await readManagedOrigin(paths.originPath)
|
|
465
|
+
if (!origin.ok) throw new Error(`Cannot enroll an unverified OpenClaw origin: ${origin.reason}`)
|
|
466
|
+
await writePrivateJson(paths.runtimePath, {
|
|
467
|
+
...baseRuntime(paths.identity, 0, null, origin, 'enrolled'),
|
|
468
|
+
})
|
|
469
|
+
await writePrivateJson(paths.enrollmentPath, {
|
|
470
|
+
schema: 1,
|
|
471
|
+
enabled: true,
|
|
472
|
+
identity: paths.identity,
|
|
473
|
+
scope,
|
|
474
|
+
originPath: paths.originPath,
|
|
475
|
+
registry: REGISTRY,
|
|
476
|
+
ownerHandle: OWNER,
|
|
477
|
+
slug: SLUG,
|
|
478
|
+
enrolledAt: new Date(now).toISOString(),
|
|
479
|
+
enrolledByCliVersion: cliVersion,
|
|
480
|
+
enrolledVersion: origin.version,
|
|
481
|
+
enrolledFingerprint: origin.fingerprint,
|
|
482
|
+
})
|
|
483
|
+
return {
|
|
484
|
+
state: 'enrolled',
|
|
485
|
+
enrollmentPath: paths.enrollmentPath,
|
|
486
|
+
runtimePath: paths.runtimePath,
|
|
487
|
+
version: origin.version,
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export async function inspectOpenClawAutoUpdate({
|
|
492
|
+
scope,
|
|
493
|
+
cwd = process.cwd(),
|
|
494
|
+
env = process.env,
|
|
495
|
+
}) {
|
|
496
|
+
try {
|
|
497
|
+
const paths = resolveOpenClawUpdatePaths({ scope, cwd, env })
|
|
498
|
+
const local = await readEnrollmentAndRuntime(paths, scope)
|
|
499
|
+
return {
|
|
500
|
+
enrollment: local.enrollmentState,
|
|
501
|
+
runtime: local.runtimeState,
|
|
502
|
+
lastAttemptAt: local.runtime?.lastAttemptAt ?? null,
|
|
503
|
+
status: local.runtime?.status ?? null,
|
|
504
|
+
fromVersion: local.runtime?.fromVersion ?? null,
|
|
505
|
+
fromFingerprint: local.runtime?.fromFingerprint ?? null,
|
|
506
|
+
candidateVersion: local.runtime?.candidateVersion ?? null,
|
|
507
|
+
candidateVerifiedAt: local.runtime?.candidateVerifiedAt ?? null,
|
|
508
|
+
reloadRequiredAt: local.runtime?.reloadRequiredAt ?? null,
|
|
509
|
+
}
|
|
510
|
+
} catch {
|
|
511
|
+
return {
|
|
512
|
+
enrollment: 'invalid',
|
|
513
|
+
runtime: 'invalid',
|
|
514
|
+
lastAttemptAt: null,
|
|
515
|
+
status: null,
|
|
516
|
+
fromVersion: null,
|
|
517
|
+
fromFingerprint: null,
|
|
518
|
+
candidateVersion: null,
|
|
519
|
+
candidateVerifiedAt: null,
|
|
520
|
+
reloadRequiredAt: null,
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function verifyLatest({ before, scope, cwd, env, deadline, run }) {
|
|
526
|
+
const scopeArgs = scope === 'global' ? ['--global'] : []
|
|
527
|
+
const verification = await run('openclaw', [
|
|
528
|
+
'skills', 'verify', OWNER_QUALIFIED_SKILL, '--tag', 'latest', ...scopeArgs,
|
|
529
|
+
], { deadline, cwd, env })
|
|
530
|
+
if (verification.code !== 0) return { ok: false }
|
|
531
|
+
let envelope
|
|
532
|
+
try {
|
|
533
|
+
envelope = JSON.parse(verification.stdout)
|
|
534
|
+
} catch {
|
|
535
|
+
return { ok: false }
|
|
536
|
+
}
|
|
537
|
+
if (!isVerificationClean(envelope, before.version)) return { ok: false }
|
|
538
|
+
return { ok: true, envelope, candidateVersion: envelope.version }
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export async function runOpenClawInvocationCheck({
|
|
542
|
+
scope,
|
|
543
|
+
cwd = process.cwd(),
|
|
544
|
+
env = process.env,
|
|
545
|
+
now = Date.now(),
|
|
546
|
+
run = runWithDeadline,
|
|
547
|
+
}) {
|
|
548
|
+
if (isAutoUpdateDisabled(env)) return { exitCode: 0, status: 'disabled' }
|
|
549
|
+
process.umask(0o077)
|
|
550
|
+
let paths
|
|
551
|
+
try {
|
|
552
|
+
paths = resolveOpenClawUpdatePaths({ scope, cwd, env })
|
|
553
|
+
const local = await readEnrollmentAndRuntime(paths, scope)
|
|
554
|
+
if (local.enrollmentState !== 'enrolled') {
|
|
555
|
+
return { exitCode: 0, status: local.enrollmentState }
|
|
556
|
+
}
|
|
557
|
+
if (local.runtimeState === 'invalid') return { exitCode: 0, status: 'invalid_runtime' }
|
|
558
|
+
if (!isDue(local.runtime, now)) return { exitCode: 0, status: 'not_due' }
|
|
559
|
+
} catch {
|
|
560
|
+
return { exitCode: 0, status: 'state_unavailable' }
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
let lock
|
|
564
|
+
try {
|
|
565
|
+
lock = await acquireLock(paths.lockPath)
|
|
566
|
+
} catch {
|
|
567
|
+
return { exitCode: 0, status: 'state_unavailable' }
|
|
568
|
+
}
|
|
569
|
+
if (!lock) return { exitCode: 0, status: 'busy' }
|
|
570
|
+
try {
|
|
571
|
+
const local = await readEnrollmentAndRuntime(paths, scope)
|
|
572
|
+
if (local.enrollmentState !== 'enrolled') return { exitCode: 0, status: 'not_enrolled' }
|
|
573
|
+
if (local.runtimeState === 'invalid') return { exitCode: 0, status: 'invalid_runtime' }
|
|
574
|
+
if (!isDue(local.runtime, now)) return { exitCode: 0, status: 'not_due' }
|
|
575
|
+
|
|
576
|
+
const before = await readManagedOrigin(paths.originPath)
|
|
577
|
+
if (!before.ok) return { exitCode: 0, status: 'origin_unavailable' }
|
|
578
|
+
const attemptedAt = new Date(now).toISOString()
|
|
579
|
+
const attempt = (local.runtime?.attempt ?? 0) + 1
|
|
580
|
+
const base = baseRuntime(paths.identity, attempt, attemptedAt, before, 'attempting')
|
|
581
|
+
try {
|
|
582
|
+
await writePrivateJson(paths.runtimePath, base)
|
|
583
|
+
} catch {
|
|
584
|
+
return { exitCode: 0, status: 'state_unavailable' }
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let verified
|
|
588
|
+
try {
|
|
589
|
+
verified = await verifyLatest({
|
|
590
|
+
before,
|
|
591
|
+
scope,
|
|
592
|
+
cwd,
|
|
593
|
+
env,
|
|
594
|
+
deadline: now + TIMEOUT_MS,
|
|
595
|
+
run,
|
|
596
|
+
})
|
|
597
|
+
} catch {
|
|
598
|
+
await writePrivateJson(paths.runtimePath, { ...base, status: 'verification_failed' }).catch(() => {})
|
|
599
|
+
return { exitCode: 0, status: 'verification_failed' }
|
|
600
|
+
}
|
|
601
|
+
if (!verified.ok) {
|
|
602
|
+
await writePrivateJson(paths.runtimePath, { ...base, status: 'verification_failed' }).catch(() => {})
|
|
603
|
+
return { exitCode: 0, status: 'verification_failed' }
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (compareSemver(verified.candidateVersion, before.version) === 0) {
|
|
607
|
+
await writePrivateJson(paths.runtimePath, {
|
|
608
|
+
...base,
|
|
609
|
+
status: 'up_to_date',
|
|
610
|
+
candidateVersion: verified.candidateVersion,
|
|
611
|
+
candidateVerifiedAt: new Date().toISOString(),
|
|
612
|
+
verificationDecision: 'pass',
|
|
613
|
+
securityStatus: 'clean',
|
|
614
|
+
}).catch(() => {})
|
|
615
|
+
return { exitCode: 0, status: 'up_to_date' }
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const candidateVerifiedAt = new Date().toISOString()
|
|
619
|
+
try {
|
|
620
|
+
await writePrivateJson(
|
|
621
|
+
paths.runtimePath,
|
|
622
|
+
pendingRuntime(base, verified.candidateVersion, candidateVerifiedAt),
|
|
623
|
+
)
|
|
624
|
+
} catch {
|
|
625
|
+
return { exitCode: 0, status: 'state_unavailable' }
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
exitCode: CONFIRMATION_EXIT_CODE,
|
|
629
|
+
status: 'pending_confirmation',
|
|
630
|
+
fromVersion: before.version,
|
|
631
|
+
candidateVersion: verified.candidateVersion,
|
|
632
|
+
message: `Deyo Skill update available: ${OWNER_QUALIFIED_SKILL} ${before.version} -> ${verified.candidateVersion}; source=ClawHub owner=${OWNER}; security=pass/clean. Ask the current user: 是否现在更新Deyo到latest?`,
|
|
633
|
+
}
|
|
634
|
+
} catch {
|
|
635
|
+
return { exitCode: 0, status: 'check_failed' }
|
|
636
|
+
} finally {
|
|
637
|
+
await lock.close().catch(() => {})
|
|
638
|
+
await rm(paths.lockPath, { force: true })
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export async function runOpenClawConfirmedUpdate({
|
|
643
|
+
scope,
|
|
644
|
+
confirmation,
|
|
645
|
+
cwd = process.cwd(),
|
|
646
|
+
env = process.env,
|
|
647
|
+
now = Date.now(),
|
|
648
|
+
run = runWithDeadline,
|
|
649
|
+
}) {
|
|
650
|
+
if (confirmation !== CONFIRM_LATEST_PHRASE) {
|
|
651
|
+
return { exitCode: 1, status: 'confirmation_required' }
|
|
652
|
+
}
|
|
653
|
+
if (isAutoUpdateDisabled(env)) return { exitCode: 0, status: 'disabled' }
|
|
654
|
+
process.umask(0o077)
|
|
655
|
+
let paths
|
|
656
|
+
try {
|
|
657
|
+
paths = resolveOpenClawUpdatePaths({ scope, cwd, env })
|
|
658
|
+
} catch {
|
|
659
|
+
return { exitCode: 0, status: 'state_unavailable' }
|
|
660
|
+
}
|
|
661
|
+
let lock
|
|
662
|
+
try {
|
|
663
|
+
lock = await acquireLock(paths.lockPath)
|
|
664
|
+
} catch {
|
|
665
|
+
return { exitCode: 0, status: 'state_unavailable' }
|
|
666
|
+
}
|
|
667
|
+
if (!lock) return { exitCode: 0, status: 'busy' }
|
|
668
|
+
try {
|
|
669
|
+
const local = await readEnrollmentAndRuntime(paths, scope)
|
|
670
|
+
if (local.enrollmentState !== 'enrolled') return { exitCode: 0, status: 'not_enrolled' }
|
|
671
|
+
if (
|
|
672
|
+
local.runtimeState !== 'valid'
|
|
673
|
+
|| local.runtime?.status !== 'pending_confirmation'
|
|
674
|
+
|| !STABLE_SEMVER.test(local.runtime?.candidateVersion || '')
|
|
675
|
+
|| !STABLE_SEMVER.test(local.runtime?.fromVersion || '')
|
|
676
|
+
|| !/^[a-f0-9]{64}$/.test(local.runtime?.fromFingerprint || '')
|
|
677
|
+
) return { exitCode: 0, status: 'confirmation_missing' }
|
|
678
|
+
|
|
679
|
+
const before = await readManagedOrigin(paths.originPath)
|
|
680
|
+
if (
|
|
681
|
+
!before.ok
|
|
682
|
+
|| before.version !== local.runtime.fromVersion
|
|
683
|
+
|| before.fingerprint !== local.runtime.fromFingerprint
|
|
684
|
+
) {
|
|
685
|
+
return {
|
|
686
|
+
exitCode: UPDATED_EXIT_CODE,
|
|
687
|
+
status: before.ok ? 'origin_changed' : 'origin_indeterminate',
|
|
688
|
+
message: 'Deyo Skill origin changed or became unreadable after confirmation; invoke /deyo again before transcription.',
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const deadline = now + TIMEOUT_MS
|
|
693
|
+
let verified
|
|
694
|
+
try {
|
|
695
|
+
verified = await verifyLatest({ before, scope, cwd, env, deadline, run })
|
|
696
|
+
} catch {
|
|
697
|
+
return { exitCode: 0, status: 'verification_failed' }
|
|
698
|
+
}
|
|
699
|
+
if (!verified.ok) return { exitCode: 0, status: 'verification_failed' }
|
|
700
|
+
if (verified.candidateVersion !== local.runtime.candidateVersion) {
|
|
701
|
+
const refreshed = pendingRuntime(
|
|
702
|
+
baseRuntime(
|
|
703
|
+
paths.identity,
|
|
704
|
+
local.runtime.attempt + 1,
|
|
705
|
+
new Date(now).toISOString(),
|
|
706
|
+
before,
|
|
707
|
+
'pending_confirmation',
|
|
708
|
+
),
|
|
709
|
+
verified.candidateVersion,
|
|
710
|
+
new Date().toISOString(),
|
|
711
|
+
)
|
|
712
|
+
try {
|
|
713
|
+
await writePrivateJson(paths.runtimePath, refreshed)
|
|
714
|
+
} catch {
|
|
715
|
+
return { exitCode: 0, status: 'state_unavailable' }
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
exitCode: CONFIRMATION_EXIT_CODE,
|
|
719
|
+
status: 'confirmation_stale',
|
|
720
|
+
fromVersion: before.version,
|
|
721
|
+
candidateVersion: verified.candidateVersion,
|
|
722
|
+
message: `Deyo Skill latest candidate changed to ${verified.candidateVersion}; source=ClawHub owner=${OWNER}; security=pass/clean. Ask the current user again: 是否现在更新Deyo到latest?`,
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const scopeArgs = scope === 'global' ? ['--global'] : []
|
|
727
|
+
let update
|
|
728
|
+
let indeterminate = false
|
|
729
|
+
try {
|
|
730
|
+
update = await run('openclaw', [
|
|
731
|
+
'skills', 'update', OWNER_QUALIFIED_SKILL, ...scopeArgs,
|
|
732
|
+
], { deadline, cwd, env })
|
|
733
|
+
} catch {
|
|
734
|
+
indeterminate = true
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const after = await readManagedOrigin(paths.originPath)
|
|
738
|
+
const changed = after.ok && (
|
|
739
|
+
after.version !== before.version || after.fingerprint !== before.fingerprint
|
|
740
|
+
)
|
|
741
|
+
const knownFailureUnchanged = !indeterminate && after.ok && !changed && update?.code !== 0
|
|
742
|
+
const reloadRequired = !knownFailureUnchanged
|
|
743
|
+
const status = indeterminate || !after.ok
|
|
744
|
+
? 'update_indeterminate'
|
|
745
|
+
: changed
|
|
746
|
+
? after.version === verified.candidateVersion ? 'updated' : 'updated_other_trusted'
|
|
747
|
+
: update?.code === 0 ? 'update_indeterminate' : 'update_failed'
|
|
748
|
+
const finalState = {
|
|
749
|
+
...baseRuntime(
|
|
750
|
+
paths.identity,
|
|
751
|
+
local.runtime.attempt + 1,
|
|
752
|
+
new Date(now).toISOString(),
|
|
753
|
+
before,
|
|
754
|
+
status,
|
|
755
|
+
),
|
|
756
|
+
status,
|
|
757
|
+
candidateVersion: verified.candidateVersion,
|
|
758
|
+
candidateVerifiedAt: new Date().toISOString(),
|
|
759
|
+
verificationDecision: 'pass',
|
|
760
|
+
securityStatus: 'clean',
|
|
761
|
+
toVersion: after.ok ? after.version : null,
|
|
762
|
+
toFingerprint: after.ok ? after.fingerprint : null,
|
|
763
|
+
...(reloadRequired ? { reloadRequiredAt: new Date().toISOString() } : {}),
|
|
764
|
+
}
|
|
765
|
+
try {
|
|
766
|
+
await writePrivateJson(paths.runtimePath, finalState)
|
|
767
|
+
} catch {
|
|
768
|
+
if (!reloadRequired) return { exitCode: 0, status: 'state_unavailable' }
|
|
769
|
+
}
|
|
770
|
+
if (reloadRequired) {
|
|
771
|
+
return {
|
|
772
|
+
exitCode: UPDATED_EXIT_CODE,
|
|
773
|
+
status,
|
|
774
|
+
fromVersion: before.version,
|
|
775
|
+
toVersion: after.ok ? after.version : null,
|
|
776
|
+
message: 'Deyo Skill may have changed; invoke Deyo again to load or re-check the active version.',
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return { exitCode: 0, status, fromVersion: before.version, toVersion: before.version }
|
|
780
|
+
} catch {
|
|
781
|
+
return { exitCode: 0, status: 'update_failed' }
|
|
782
|
+
} finally {
|
|
783
|
+
await lock.close().catch(() => {})
|
|
784
|
+
await rm(paths.lockPath, { force: true })
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
export const __test = {
|
|
789
|
+
CONFIRM_LATEST_PHRASE,
|
|
790
|
+
CONFIRMATION_EXIT_CODE,
|
|
791
|
+
OWNER_QUALIFIED_SKILL,
|
|
792
|
+
UPDATED_EXIT_CODE,
|
|
793
|
+
writePrivateJson,
|
|
794
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@casatwy/deyo",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Deyo transcription CLI with link/file input, JSONL progress, and opt-in stable transcript streaming",
|
|
5
5
|
"homepage": "https://deyo.miaobi.fun/docs",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"files": [
|
|
9
9
|
"deyo.mjs",
|
|
10
10
|
"skill-manager.mjs",
|
|
11
|
+
"openclaw-skill-update.mjs",
|
|
11
12
|
"README.md"
|
|
12
13
|
],
|
|
13
14
|
"keywords": [
|
|
@@ -28,4 +29,4 @@
|
|
|
28
29
|
"bin": {
|
|
29
30
|
"deyo": "deyo.mjs"
|
|
30
31
|
}
|
|
31
|
-
}
|
|
32
|
+
}
|
package/skill-manager.mjs
CHANGED
|
@@ -16,6 +16,13 @@ import { createHash, randomBytes } from 'node:crypto'
|
|
|
16
16
|
import { homedir } from 'node:os'
|
|
17
17
|
import { basename, dirname, isAbsolute, join, normalize, resolve } from 'node:path'
|
|
18
18
|
import process from 'node:process'
|
|
19
|
+
import {
|
|
20
|
+
CONFIRM_LATEST_PHRASE,
|
|
21
|
+
enrollOpenClawAutoUpdate,
|
|
22
|
+
inspectOpenClawAutoUpdate,
|
|
23
|
+
runOpenClawConfirmedUpdate,
|
|
24
|
+
runOpenClawInvocationCheck,
|
|
25
|
+
} from './openclaw-skill-update.mjs'
|
|
19
26
|
|
|
20
27
|
export const SKILL_PLATFORMS = ['codex', 'claude', 'gemini', 'openclaw']
|
|
21
28
|
export const SKILL_SCOPES = ['user', 'workspace', 'global']
|
|
@@ -25,6 +32,8 @@ const PLUGIN_ID = `deyo@${MARKETPLACE_NAME}`
|
|
|
25
32
|
const GITHUB_REPOSITORY = 'casatwy/deyo-skill'
|
|
26
33
|
const GIT_SOURCE_URL = 'https://github.com/casatwy/deyo-skill.git'
|
|
27
34
|
const OPENCLAW_SKILL = '@casatwy/deyo'
|
|
35
|
+
const OPENCLAW_REGISTRY = 'https://clawhub.ai'
|
|
36
|
+
const STABLE_SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/
|
|
28
37
|
|
|
29
38
|
const DEFAULT_SCOPES = {
|
|
30
39
|
codex: 'user',
|
|
@@ -187,13 +196,17 @@ export function printSkillUsage(stream = process.stdout) {
|
|
|
187
196
|
[--replace-legacy sha256:<observed-hash>]
|
|
188
197
|
[--json]
|
|
189
198
|
deyo skill status [--platform codex|claude|gemini|openclaw] [--json]
|
|
199
|
+
deyo skill update --platform openclaw [--scope workspace|global]
|
|
200
|
+
--confirm-latest 'update @casatwy/deyo to latest now'
|
|
190
201
|
|
|
191
202
|
默认 scope:
|
|
192
203
|
codex=user,claude=user,gemini=user,openclaw=global
|
|
193
204
|
|
|
194
205
|
说明:
|
|
195
206
|
status 只读取本地配置和 manifest,不联网、不检查更新,也不修改 cache/lockfile。
|
|
196
|
-
install
|
|
207
|
+
install 只调用各平台原生管理器;OpenClaw 安装成功后登记调用时检查,不会自动更新已有安装。
|
|
208
|
+
update 仅用于当前用户明确同意后更新 OpenClaw;执行前会重新验证 latest,候选变化时必须重新确认。
|
|
209
|
+
所有原生命令都不会使用 --force、--all、--force-install、风险确认、固定 --ref 或 --consent。
|
|
197
210
|
legacy 目录只有在完整树哈希属于官方历史版本,且 --replace-legacy 精确匹配时才会备份迁移。
|
|
198
211
|
`)
|
|
199
212
|
}
|
|
@@ -206,6 +219,7 @@ function parseSkillCommandArgs(argv) {
|
|
|
206
219
|
platform: '',
|
|
207
220
|
scope: '',
|
|
208
221
|
replaceLegacy: '',
|
|
222
|
+
confirmLatest: '',
|
|
209
223
|
json: false,
|
|
210
224
|
help: requestedAction === '-h' || requestedAction === '--help',
|
|
211
225
|
}
|
|
@@ -255,13 +269,19 @@ function parseSkillCommandArgs(argv) {
|
|
|
255
269
|
index += 1
|
|
256
270
|
continue
|
|
257
271
|
}
|
|
272
|
+
if (current === '--confirm-latest') {
|
|
273
|
+
markSeen(current)
|
|
274
|
+
result.confirmLatest = readOptionValue(current, index)
|
|
275
|
+
index += 1
|
|
276
|
+
continue
|
|
277
|
+
}
|
|
258
278
|
throw new SkillManagerError(`未知 skill 参数:${current}`, 'unknown_argument')
|
|
259
279
|
}
|
|
260
280
|
|
|
261
281
|
if (!action && !result.help) {
|
|
262
282
|
throw new SkillManagerError('缺少 skill 子命令 install 或 status', 'missing_action')
|
|
263
283
|
}
|
|
264
|
-
if (!['install', 'status'].includes(action) && !result.help) {
|
|
284
|
+
if (!['install', 'status', 'update', '_openclaw-check'].includes(action) && !result.help) {
|
|
265
285
|
throw new SkillManagerError(`不支持的 skill 子命令:${action}`, 'invalid_action')
|
|
266
286
|
}
|
|
267
287
|
if (action === 'install' && !result.platform && !result.help) {
|
|
@@ -273,6 +293,38 @@ function parseSkillCommandArgs(argv) {
|
|
|
273
293
|
if (action === 'status' && result.replaceLegacy) {
|
|
274
294
|
throw new SkillManagerError('skill status 不接受 --replace-legacy', 'status_replace_legacy')
|
|
275
295
|
}
|
|
296
|
+
if (action === 'status' && result.confirmLatest) {
|
|
297
|
+
throw new SkillManagerError('skill status 不接受 --confirm-latest', 'status_confirm_latest')
|
|
298
|
+
}
|
|
299
|
+
if (action === 'install' && result.confirmLatest) {
|
|
300
|
+
throw new SkillManagerError('skill install 不接受 --confirm-latest', 'install_confirm_latest')
|
|
301
|
+
}
|
|
302
|
+
if (action === 'update' && result.platform !== 'openclaw') {
|
|
303
|
+
throw new SkillManagerError('skill update 目前只支持 --platform openclaw', 'update_platform')
|
|
304
|
+
}
|
|
305
|
+
if (action === 'update' && result.replaceLegacy) {
|
|
306
|
+
throw new SkillManagerError('skill update 不接受 --replace-legacy', 'update_replace_legacy')
|
|
307
|
+
}
|
|
308
|
+
if (action === 'update' && result.json) {
|
|
309
|
+
throw new SkillManagerError('skill update 不接受 --json;更新输出不会写入 stdout', 'update_json')
|
|
310
|
+
}
|
|
311
|
+
if (action === 'update' && !result.confirmLatest) {
|
|
312
|
+
throw new SkillManagerError('skill update 必须提供 --confirm-latest', 'confirmation_required')
|
|
313
|
+
}
|
|
314
|
+
if (action === 'update' && result.confirmLatest !== CONFIRM_LATEST_PHRASE) {
|
|
315
|
+
throw new SkillManagerError(
|
|
316
|
+
`--confirm-latest 必须精确为 '${CONFIRM_LATEST_PHRASE}'`,
|
|
317
|
+
'confirmation_required',
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
if (action === '_openclaw-check') {
|
|
321
|
+
if (result.platform || result.replaceLegacy || result.confirmLatest || result.json) {
|
|
322
|
+
throw new SkillManagerError('内部 OpenClaw 检查只接受 --scope', 'internal_check_arguments')
|
|
323
|
+
}
|
|
324
|
+
if (!result.scope) {
|
|
325
|
+
throw new SkillManagerError('内部 OpenClaw 检查必须提供 --scope', 'missing_scope')
|
|
326
|
+
}
|
|
327
|
+
}
|
|
276
328
|
|
|
277
329
|
return result
|
|
278
330
|
}
|
|
@@ -1010,15 +1062,15 @@ function getOpenClawSkillPath(scope) {
|
|
|
1010
1062
|
|
|
1011
1063
|
function isOfficialOpenClawOrigin(origin) {
|
|
1012
1064
|
if (!isPlainObject(origin)) return false
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
&&
|
|
1065
|
+
const registry = typeof origin.registry === 'string' ? origin.registry.replace(/\/+$/, '') : ''
|
|
1066
|
+
return origin.version === 1
|
|
1067
|
+
&& registry === OPENCLAW_REGISTRY
|
|
1068
|
+
&& String(origin.ownerHandle || '').replace(/^@/, '').toLowerCase() === 'casatwy'
|
|
1069
|
+
&& String(origin.slug || '').replace(/^@casatwy\//, '').toLowerCase() === 'deyo'
|
|
1070
|
+
&& typeof origin.installedVersion === 'string'
|
|
1071
|
+
&& STABLE_SEMVER.test(origin.installedVersion)
|
|
1072
|
+
&& Number.isFinite(origin.installedAt)
|
|
1073
|
+
&& /^[a-f0-9]{64}$/.test(origin.fingerprint || '')
|
|
1022
1074
|
}
|
|
1023
1075
|
|
|
1024
1076
|
async function inspectOpenClawSkill(scope) {
|
|
@@ -1055,12 +1107,12 @@ async function inspectOpenClawSkill(scope) {
|
|
|
1055
1107
|
managed: metadata.exists,
|
|
1056
1108
|
officialSource: metadata.safe && isOfficialOpenClawOrigin(origin),
|
|
1057
1109
|
origin,
|
|
1058
|
-
version: origin
|
|
1110
|
+
version: isOfficialOpenClawOrigin(origin) ? origin.installedVersion : null,
|
|
1059
1111
|
safe: metadata.safe,
|
|
1060
1112
|
}
|
|
1061
1113
|
}
|
|
1062
1114
|
|
|
1063
|
-
async function buildOpenClawInstallPlan(scope, jsonMode) {
|
|
1115
|
+
async function buildOpenClawInstallPlan(scope, jsonMode, cliVersion) {
|
|
1064
1116
|
const openclaw = await requireExecutable('openclaw')
|
|
1065
1117
|
const skill = await inspectOpenClawSkill(scope)
|
|
1066
1118
|
if (skill.managed && !skill.officialSource) {
|
|
@@ -1072,24 +1124,41 @@ async function buildOpenClawInstallPlan(scope, jsonMode) {
|
|
|
1072
1124
|
|
|
1073
1125
|
return async () => {
|
|
1074
1126
|
const scopeArgs = scope === 'global' ? ['--global'] : []
|
|
1127
|
+
const actions = []
|
|
1075
1128
|
if (skill.managed) {
|
|
1129
|
+
actions.push('already_installed')
|
|
1130
|
+
} else {
|
|
1076
1131
|
await runManager(openclaw, [
|
|
1077
|
-
'skills', '
|
|
1132
|
+
'skills', 'install', OPENCLAW_SKILL, ...scopeArgs,
|
|
1078
1133
|
], { interactive: true, jsonMode })
|
|
1079
|
-
|
|
1134
|
+
actions.push('skill_installed')
|
|
1080
1135
|
}
|
|
1081
|
-
await
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1136
|
+
const installed = await inspectOpenClawSkill(scope)
|
|
1137
|
+
if (!installed.installed || !installed.officialSource) {
|
|
1138
|
+
throw new SkillManagerError(
|
|
1139
|
+
'OpenClaw 原生安装已返回,但无法从当前 scope 的 origin.json 确认 @casatwy/deyo',
|
|
1140
|
+
'openclaw_origin_unverified',
|
|
1141
|
+
)
|
|
1142
|
+
}
|
|
1143
|
+
let autoUpdate
|
|
1144
|
+
try {
|
|
1145
|
+
autoUpdate = await enrollOpenClawAutoUpdate({ scope, cliVersion })
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
throw new SkillManagerError(
|
|
1148
|
+
`OpenClaw Skill 已处理,但调用时更新检查登记失败:${error instanceof Error ? error.message : String(error)}`,
|
|
1149
|
+
'openclaw_enrollment_failed',
|
|
1150
|
+
)
|
|
1151
|
+
}
|
|
1152
|
+
actions.push('invocation_update_check_enrolled')
|
|
1153
|
+
return { actions, activation: 'next_invocation', autoUpdate }
|
|
1085
1154
|
}
|
|
1086
1155
|
}
|
|
1087
1156
|
|
|
1088
|
-
async function buildPlatformInstallPlan(platform, scope, jsonMode) {
|
|
1157
|
+
async function buildPlatformInstallPlan(platform, scope, jsonMode, cliVersion) {
|
|
1089
1158
|
if (platform === 'codex') return await buildCodexInstallPlan(jsonMode)
|
|
1090
1159
|
if (platform === 'claude') return await buildClaudeInstallPlan(scope, jsonMode)
|
|
1091
1160
|
if (platform === 'gemini') return await buildGeminiInstallPlan(jsonMode)
|
|
1092
|
-
return await buildOpenClawInstallPlan(scope, jsonMode)
|
|
1161
|
+
return await buildOpenClawInstallPlan(scope, jsonMode, cliVersion)
|
|
1093
1162
|
}
|
|
1094
1163
|
|
|
1095
1164
|
function extractTomlSection(content, sectionName) {
|
|
@@ -1185,12 +1254,19 @@ async function inspectPlatformManaged(platform, scope) {
|
|
|
1185
1254
|
}
|
|
1186
1255
|
}
|
|
1187
1256
|
const skill = await inspectOpenClawSkill(scope)
|
|
1257
|
+
const update = await inspectOpenClawAutoUpdate({
|
|
1258
|
+
scope,
|
|
1259
|
+
...(scope === 'workspace' ? { cwd: process.cwd() } : {}),
|
|
1260
|
+
})
|
|
1188
1261
|
return {
|
|
1189
1262
|
installed: skill.installed,
|
|
1190
1263
|
version: skill.version,
|
|
1191
1264
|
source: skill.origin,
|
|
1192
1265
|
officialSource: skill.officialSource,
|
|
1193
|
-
autoUpdate: '
|
|
1266
|
+
autoUpdate: update.enrollment === 'enrolled'
|
|
1267
|
+
? 'confirmation_required_on_invocation'
|
|
1268
|
+
: update.enrollment,
|
|
1269
|
+
autoUpdateState: update,
|
|
1194
1270
|
installPath: skill.path,
|
|
1195
1271
|
}
|
|
1196
1272
|
}
|
|
@@ -1214,7 +1290,7 @@ async function getSkillStatus(platform) {
|
|
|
1214
1290
|
}
|
|
1215
1291
|
}
|
|
1216
1292
|
|
|
1217
|
-
async function installSkill(options) {
|
|
1293
|
+
async function installSkill(options, cliVersion) {
|
|
1218
1294
|
validatePlatform(options.platform)
|
|
1219
1295
|
const scope = options.scope || getDefaultScope(options.platform)
|
|
1220
1296
|
validateScope(options.platform, scope)
|
|
@@ -1222,7 +1298,7 @@ async function installSkill(options) {
|
|
|
1222
1298
|
|
|
1223
1299
|
// All source-conflict and manager checks happen before the legacy directory
|
|
1224
1300
|
// is moved or any provider configuration is mutated.
|
|
1225
|
-
const execute = await buildPlatformInstallPlan(options.platform, scope, options.json)
|
|
1301
|
+
const execute = await buildPlatformInstallPlan(options.platform, scope, options.json, cliVersion)
|
|
1226
1302
|
const legacy = await inspectLegacy(options.platform, scope)
|
|
1227
1303
|
let migration = null
|
|
1228
1304
|
try {
|
|
@@ -1288,6 +1364,7 @@ function printStatusResult(result) {
|
|
|
1288
1364
|
|
|
1289
1365
|
export async function handleSkillCommand(argv, cliVersion) {
|
|
1290
1366
|
const wantsJson = argv.includes('--json')
|
|
1367
|
+
const isolatesStdout = argv[1] === 'update' || argv[1] === '_openclaw-check'
|
|
1291
1368
|
try {
|
|
1292
1369
|
const args = parseSkillCommandArgs(argv)
|
|
1293
1370
|
if (args.help) {
|
|
@@ -1297,13 +1374,32 @@ export async function handleSkillCommand(argv, cliVersion) {
|
|
|
1297
1374
|
if (args.platform) validatePlatform(args.platform)
|
|
1298
1375
|
|
|
1299
1376
|
if (args.action === 'install') {
|
|
1300
|
-
const result = await installSkill(args)
|
|
1377
|
+
const result = await installSkill(args, cliVersion)
|
|
1301
1378
|
const output = { cliVersion, ...result }
|
|
1302
1379
|
if (args.json) process.stdout.write(`${JSON.stringify(output)}\n`)
|
|
1303
1380
|
else printInstallResult(output)
|
|
1304
1381
|
return
|
|
1305
1382
|
}
|
|
1306
1383
|
|
|
1384
|
+
if (args.action === '_openclaw-check') {
|
|
1385
|
+
const scope = args.scope
|
|
1386
|
+
validateScope('openclaw', scope)
|
|
1387
|
+
const result = await runOpenClawInvocationCheck({ scope })
|
|
1388
|
+
if (result.message) process.stderr.write(`${result.message}\n`)
|
|
1389
|
+
return result.exitCode
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
if (args.action === 'update') {
|
|
1393
|
+
const scope = args.scope || getDefaultScope('openclaw')
|
|
1394
|
+
validateScope('openclaw', scope)
|
|
1395
|
+
const result = await runOpenClawConfirmedUpdate({
|
|
1396
|
+
scope,
|
|
1397
|
+
confirmation: args.confirmLatest,
|
|
1398
|
+
})
|
|
1399
|
+
if (result.message) process.stderr.write(`${result.message}\n`)
|
|
1400
|
+
return result.exitCode
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1307
1403
|
const platforms = args.platform ? [args.platform] : SKILL_PLATFORMS
|
|
1308
1404
|
const result = {
|
|
1309
1405
|
command: 'skill.status',
|
|
@@ -1316,13 +1412,14 @@ export async function handleSkillCommand(argv, cliVersion) {
|
|
|
1316
1412
|
}
|
|
1317
1413
|
if (args.json) process.stdout.write(`${JSON.stringify(result)}\n`)
|
|
1318
1414
|
else printStatusResult(result)
|
|
1415
|
+
return 0
|
|
1319
1416
|
} catch (error) {
|
|
1320
1417
|
const payload = {
|
|
1321
1418
|
command: 'skill.error',
|
|
1322
1419
|
error: error?.code || 'skill_error',
|
|
1323
1420
|
message: error instanceof Error ? error.message : String(error),
|
|
1324
1421
|
}
|
|
1325
|
-
if (wantsJson) process.stdout.write(`${JSON.stringify(payload)}\n`)
|
|
1422
|
+
if (wantsJson && !isolatesStdout) process.stdout.write(`${JSON.stringify(payload)}\n`)
|
|
1326
1423
|
else process.stderr.write(`Deyo Skill 操作失败:${payload.message}\n`)
|
|
1327
1424
|
throw new SkillCommandReportedError(1)
|
|
1328
1425
|
}
|