mirror of
https://github.com/kuaifan/dootask.git
synced 2026-06-11 09:52:26 +00:00
feat(skill): 新增 dootask-release 发版技能
翻译与更新日志在技能内直接产出,版本号计算/差异检测/语言文件生成等 机械步骤交给本地脚本(language.php、version_bump.js,host 直跑、不进容器)。 language.php 用 php 以字节级对齐项目原生产物;脚本相对自身定位项目根,与 cwd 无关。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
36da18af79
commit
035c9d9d3d
@ -1,6 +1,6 @@
|
||||
---
|
||||
name: dootask-release
|
||||
description: 从 `pro` 分支发布 DooTask 前端新版本:刚性顺序流程 translate → version → build → commit → push,前置检查 + 每步确认、失败即停。
|
||||
description: 从 `pro` 分支发布 DooTask 前端新版本:翻译 → 版本号/更新日志 → 构建 → 提交推送,刚性顺序、每步确认、失败即停。
|
||||
---
|
||||
|
||||
# DooTask 发布流程
|
||||
@ -9,7 +9,7 @@ description: 从 `pro` 分支发布 DooTask 前端新版本:刚性顺序流程
|
||||
|
||||
## 核心原则
|
||||
|
||||
**违反字面规则 = 违反流程精神。** 不要擅自增加、省略、合并或重排步骤。
|
||||
按固定顺序执行,不增删、合并或重排步骤。翻译(Step 1)和更新日志(Step 2)由你直接产出;脚本只做确定性机械工作(算版本号、检测差异、字节级生成语言文件)。
|
||||
|
||||
## 前置检查(全部通过才能继续)
|
||||
|
||||
@ -17,7 +17,8 @@ description: 从 `pro` 分支发布 DooTask 前端新版本:刚性顺序流程
|
||||
|
||||
1. **分支**:必须是 `pro`,否则停止,提示用户切换
|
||||
2. **工作区**:`git status` 必须干净(无未提交变更、无未跟踪文件),否则**停止**并交由用户处理
|
||||
3. **Node.js**:必须 ≥ 20,否则停止
|
||||
3. **Node.js**:`node --version` 必须 ≥ 20
|
||||
4. **PHP**:`php --version` 必须可用(Step 1 的脚本依赖本地 php,无需容器)。若 host 无 php,停止并提示用户
|
||||
|
||||
检查通过后汇报结果,用户确认后再开始执行。
|
||||
|
||||
@ -25,29 +26,140 @@ description: 从 `pro` 分支发布 DooTask 前端新版本:刚性顺序流程
|
||||
|
||||
**每步执行前**向用户确认;**每步执行后**报告结果。
|
||||
|
||||
### Step 1: 翻译
|
||||
```shell
|
||||
npm run translate
|
||||
```
|
||||
更新多语言翻译文件。
|
||||
开始前先把这份清单复制到你的回复里,逐项勾选、跟踪进度:
|
||||
|
||||
### Step 2: 版本号
|
||||
```shell
|
||||
npm run version
|
||||
```
|
||||
更新版本号。
|
||||
发布进度:
|
||||
- [ ] 前置检查(分支 pro / 工作区干净 / node≥20 / php 可用)
|
||||
- [ ] Step 1 翻译(diff → 翻译 → apply → generate)
|
||||
- [ ] Step 2 版本号 + CHANGELOG
|
||||
- [ ] Step 3 构建(./cmd prod)
|
||||
- [ ] 汇总变更 → 用户确认 → commit + push
|
||||
- [ ] 确认 GitHub Actions Publish 工作流 success
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 1: 翻译
|
||||
|
||||
多语言数据流:`language/original-{web,api}.txt`(原文/简体中文)→ 经翻译写入 `language/translate.json`(含 9 种语言)→ 生成 `public/language/{web,api}/*`。
|
||||
|
||||
**1.1 检测差异**
|
||||
|
||||
```shell
|
||||
php .claude/skills/dootask-release/scripts/language.php diff
|
||||
```
|
||||
|
||||
输出 JSON:
|
||||
- `regexErrorCount > 0`:translate.json **已有条目**的占位符与某语言值不一致 → **停止**,报告 `regexErrors`,交用户修复(这是历史数据问题,不要自行猜测修改)
|
||||
- `redundantCount > 0`:translate.json 里有、但原文已删除的条目 → 仅作提示(apply 时会自动剔除,不致命)
|
||||
- `needsCount == 0`:无新文案 → **跳到 1.4 直接生成**
|
||||
- `needsCount > 0`:`needs` 数组即待翻译清单,每项 `key` 已转成占位符形式(如 `(%T1)`)→ 进入 1.2
|
||||
|
||||
**1.2 翻译**
|
||||
|
||||
对 `needs` 里的每个 `key`,翻成 8 种语言(`zh` 留空、`key` 原样保留):`zh-CHT` `en` `ko` `ja` `de` `fr` `id` `ru`。
|
||||
|
||||
要求:贴合「项目任务管理系统」语境;占位符 `(%T1)`/`(%M1)` 等原样保留、不可增删改,位置可随目标语言语序调整:
|
||||
|
||||
| 原文 | 翻成英语 |
|
||||
|---|---|
|
||||
| (%T1)的周报[(%T2)][(%T3)月第(%T4)周] | Weekly report of (%T1) [(%T2)] [Week (%T4) of month (%T3)] |
|
||||
| (%T1)提交的「(%M2)」待你审批 | '(%M2)' submitted by (%T1) is waiting for your approval |
|
||||
|
||||
把结果写成一个 JSON 数组文件(建议放 `/tmp/dootask-release-translated.json`,避免污染工作区),每个元素含全部 10 个字段,顺序为:
|
||||
`key, zh, zh-CHT, en, ko, ja, de, fr, id, ru`(`zh` 写 `""`)。
|
||||
|
||||
```json
|
||||
[
|
||||
{"key":"...(%T1)...","zh":"","zh-CHT":"...","en":"...","ko":"...","ja":"...","de":"...","fr":"...","id":"...","ru":"..."}
|
||||
]
|
||||
```
|
||||
|
||||
**1.3 合并进 translate.json**
|
||||
|
||||
```shell
|
||||
php .claude/skills/dootask-release/scripts/language.php apply /tmp/dootask-release-translated.json
|
||||
```
|
||||
|
||||
脚本会校验字段完整性与占位符完整性、追加新条目、剔除冗余项,并按项目原生格式写回 `translate.json`。任一条不合格会报错停止,按提示修正翻译后重试。
|
||||
|
||||
**1.4 生成前端/后端语言文件**
|
||||
|
||||
```shell
|
||||
php .claude/skills/dootask-release/scripts/language.php generate
|
||||
```
|
||||
|
||||
由 `translate.json` 字节级重新生成 `public/language/web/*.js` 与 `public/language/api/*.json`(排序/转义与项目原生工具完全一致,正常情况下 diff 只包含本次新增条目)。
|
||||
|
||||
**1.5 报告**:用 `git status --short language public/language` 汇总本步改动,向用户报告新增了多少条翻译。
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 版本号 + 更新日志
|
||||
|
||||
**2.1 计算并写入版本号**
|
||||
|
||||
```shell
|
||||
node .claude/skills/dootask-release/scripts/version_bump.js
|
||||
```
|
||||
|
||||
脚本据 git 历史算出新 `version` 与 `codeVerson` 并写入 `package.json`,输出 JSON 含:`version`、`prevVersion`、`changelogRange`(如 `<上次release提交>..HEAD`,用于下一步圈定本次更新范围)。
|
||||
|
||||
**2.2 撰写 CHANGELOG**
|
||||
|
||||
读取本次区间的提交:
|
||||
|
||||
```shell
|
||||
git log <changelogRange> --stat
|
||||
```
|
||||
|
||||
`--stat` 会带上每个提交的完整描述正文 + 改动文件清单;光看标题不够时用 `git show <hash>` 看具体代码改动。
|
||||
|
||||
按 `CHANGELOG.md` 现有格式,在文件顶部 `# Changelog` 说明段之后、紧挨上一个 `## [...]` 之前,插入新版本区段:
|
||||
|
||||
```markdown
|
||||
## [<version>]
|
||||
|
||||
### Features
|
||||
|
||||
- ...
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- ...
|
||||
|
||||
### Performance
|
||||
|
||||
- ...
|
||||
```
|
||||
|
||||
撰写要求(对齐项目历史风格):
|
||||
- 小节标题用**英文 Title Case**:`Features` / `Bug Fixes` / `Performance` / `Documentation` / `Security` / `Miscellaneous`,**不要译成中文**;**没有内容的小节整段省略**。
|
||||
- 条目正文用**通俗友好的简体中文**,面向**普通用户**描述更新带来的直接好处,**避免技术术语**(如 refactor、merge branch、commit lint、bump deps 等)。
|
||||
- 过滤掉对用户无意义的提交(纯构建/依赖/CI/合并提交、本技能自身的脚手架改动等)。
|
||||
- 仅凭提交标题无法判断是否对用户有价值时,结合提交的完整描述正文和实际代码改动(`git show <hash>`)再决定,不要只看一行就下结论。
|
||||
- 合并相似项;每个小节内**按用户价值与影响范围排序,重要的在前**。
|
||||
|
||||
**2.3 报告**:展示新版本号与你写的 changelog 区段,请用户过目。
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 构建前端
|
||||
```shell
|
||||
npm run build
|
||||
```
|
||||
构建前端生产版本。
|
||||
|
||||
> **已知失败**:若 build 报 `public/uploads/...` 的 `EACCES: permission denied, copyfile`,是 vite 复制 `public/` 目录时碰到运行时残留的上传文件——这些文件常为 **root 属主**(容器内 root 进程写入),复制需覆盖写入,构建用户没有写权限。报错路径可能落在 `public/uploads` 下任意子目录(`tmp`、`avatar` 等),不限于 `tmp`。这不是代码问题。**补救(赋权,不删数据)**:把整个 uploads 的属主改回当前用户后重试 build:
|
||||
```shell
|
||||
./cmd prod
|
||||
```
|
||||
|
||||
构建前端生产版本。用 `./cmd prod`,不要换成裸跑 vite(它还负责 node 检查、清 `public/js/build`、debug 切换)。
|
||||
|
||||
> **已知失败**:build 报 `public/uploads/...` 的 `EACCES: permission denied, copyfile`,是 vite 复制 `public/` 时撞到 root 属主的运行时上传文件(不限于 `tmp`,`avatar` 等都可能)。补救是赋权、不是删数据——把 uploads 属主改回当前用户后重试:
|
||||
> ```shell
|
||||
> sudo chown -R "$(id -u):$(id -g)" public/uploads
|
||||
> ```
|
||||
> 需 root(本机可免密 sudo;或经 docker 以 root 改权限)。**优先赋权,不要删**——`public/uploads` 含真实上传数据。即便用户要求清理,也只清临时目录 `public/uploads/tmp`,切勿删 uploads 下其他内容。
|
||||
> `public/uploads` 是真实上传数据,**不要删**;即便要清也只清 `public/uploads/tmp`。
|
||||
|
||||
---
|
||||
|
||||
## 最终:提交并推送
|
||||
|
||||
@ -59,8 +171,10 @@ npm run build
|
||||
4. 未确认一律不执行
|
||||
|
||||
提交规范:
|
||||
- 提交信息使用 `release: v<新版本号>`(与历史提交风格一致,参见 `git log --oneline | grep '^release:'`)
|
||||
- **只 add 本次发布相关改动**,按文件名显式添加(例如 `git add package.json public/js/...`),**不要用 `git add -A` 或 `git add .`**,以免卷入未跟踪的本地实验文件
|
||||
- 提交信息使用 `release: v<新版本号>`(与历史一致,参见 `git log --oneline | grep '^release:'`)
|
||||
- **只 add 本次发布相关改动**,按文件名/目录显式添加(例如 `git add package.json CHANGELOG.md language/translate.json public/language public/js`),不要用 `git add -A` / `git add .`,以免卷入未跟踪的本地实验文件
|
||||
- 不打 git tag(现行发布流程不使用 tag)
|
||||
- 确认前先核对:`/tmp/dootask-release-translated.json` 等临时文件不在仓库内,工作区不应残留发布无关的未跟踪文件
|
||||
|
||||
## push 之后:确认发布工作流(CI 才是真正出包)
|
||||
|
||||
@ -74,38 +188,17 @@ push 到 `pro` 只是触发器,真正的构建/出包由 GitHub Actions 完成
|
||||
```
|
||||
- 工作流仍在跑时,挂后台轮询、结束即通知用户,**不要在前台死等**。
|
||||
|
||||
### 可选:iOS 发布
|
||||
### iOS 发布(询问后决定)
|
||||
|
||||
`ios-publish.yml` 是**独立的手动工作流**(`workflow_dispatch`),不随 push 触发。**仅当用户明确要求**发 iOS 时执行:
|
||||
```shell
|
||||
gh workflow run ios-publish.yml --ref pro -R kuaifan/dootask
|
||||
```
|
||||
需 `gh` 已登录且 token 含 `workflow` 权限。触发后同样可挂后台轮询其结果。
|
||||
`ios-publish.yml` 是**独立的手动工作流**(`workflow_dispatch`),不随 push 触发。Publish 成功后,用 options 或 AskUserQuestion 形式提问是否同时发布 iOS(选项:发布 iOS / 不发布):
|
||||
|
||||
- 选「发布 iOS」才执行:
|
||||
```shell
|
||||
gh workflow run ios-publish.yml --ref pro -R kuaifan/dootask
|
||||
```
|
||||
需 `gh` 已登录且 token 含 `workflow` 权限;触发后可挂后台轮询结果。
|
||||
- 选「不发布」则结束。
|
||||
|
||||
## 失败处理
|
||||
|
||||
- 任何步骤失败立即停止,报告错误信息
|
||||
- **不要**自动重试
|
||||
- **不要**自动跳过失败步骤
|
||||
- 由用户决定如何处理
|
||||
|
||||
## 禁止项(基线测试暴露的反模式)
|
||||
|
||||
| 错误做法 | 正确做法 |
|
||||
|---------|---------|
|
||||
| 遇到脏工作区主动提出修复方案(加 `.gitignore`、先 push 等) | **停下**,报告脏工作区事实,交用户决定 |
|
||||
| 增加 `git tag v1.7.xx` 步骤 | DooTask 现行发布流程**不打 tag**,不要擅自添加 |
|
||||
| `git add -A` / `git add .` | 按文件名显式添加发布相关改动 |
|
||||
| 一次性 add + commit + push,不给确认机会 | 摘要 → 问确认 → 再 add/commit/push 三步分离 |
|
||||
| 把 translate/version/build 顺序自作主张调整 | 顺序固定为 translate → version → build |
|
||||
| 失败后"我再试一次"或"跳过这步" | 立即停止,交还给用户 |
|
||||
|
||||
## Red Flags —— 出现这些念头立即停下
|
||||
|
||||
- "这个脏工作区我来帮 TA 搞定一下" → 停下,交用户
|
||||
- "顺便打个 tag 吧" → 不,没有这一步
|
||||
- "`git add -A` 省事" → 不,显式 add
|
||||
- "翻译这步没改动可以跳" → 不,按顺序执行、执行后报告结果即可
|
||||
- "一起 commit + push 一气呵成" → 必须先让用户确认
|
||||
- "push 上去了,发布就完成了" → 不,push 只是触发器,要确认 GitHub Actions 的 Publish 工作流 success
|
||||
- "build 报 uploads 权限错,我直接删掉" → 优先 `chown` 赋权整个 `public/uploads`(不丢数据);真要删也只删 `tmp`,别碰 uploads 下真实上传数据
|
||||
任何步骤失败立即停止、报告错误信息,交用户决定;不要自动重试或跳过。
|
||||
|
||||
239
.claude/skills/dootask-release/scripts/language.php
Normal file
239
.claude/skills/dootask-release/scripts/language.php
Normal file
@ -0,0 +1,239 @@
|
||||
<?php
|
||||
// DooTask 发布——翻译流水线(纯本地 php,host 直接跑,不进容器、不调 OpenAI、不需 autoload)。
|
||||
// 逐行对齐 language/translate.php 的检测/保存/生成逻辑,唯独把"调用外部模型翻译"那一段抽走,
|
||||
// 翻译改在技能流程内完成。用 php 而非 node 的唯一原因:array_multisort + json_encode
|
||||
// 的逐字节产物必须与项目原生工具一致,否则每次发版都会产生大面积排序/转义噪声 diff(已验证 host php 可字节级复现)。
|
||||
//
|
||||
// 子命令:
|
||||
// language.php diff
|
||||
// —— 输出 JSON:needs(待翻译,key 已转成 (%T1)/(%M1) 形式) / redundants(冗余,提示) / regexErrors(占位符错乱,致命)
|
||||
// language.php apply <translated.json>
|
||||
// —— 把新翻译合并进 translate.json(追加 + 剔除冗余),不生成 public 文件
|
||||
// language.php generate
|
||||
// —— 由 translate.json 重新生成 public/language/{web,api}/*
|
||||
//
|
||||
// 项目根相对脚本自身定位(脚本固定在 <root>/.claude/skills/dootask-release/scripts/),与调用时的 cwd 无关。
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
$ROOT = dirname(__DIR__, 4);
|
||||
$LANG_DIR = $ROOT . '/language';
|
||||
$LANG_FIELDS = ['key', 'zh', 'zh-CHT', 'en', 'ko', 'ja', 'de', 'fr', 'id', 'ru'];
|
||||
|
||||
if (!is_dir($LANG_DIR)) {
|
||||
fwrite(STDERR, "未找到 language 目录($LANG_DIR)。\n");
|
||||
exit(1);
|
||||
}
|
||||
chdir($LANG_DIR);
|
||||
|
||||
$cmd = $argv[1] ?? '';
|
||||
|
||||
// ---- 公共:读取 original-*.txt ----
|
||||
function read_generateds(): array
|
||||
{
|
||||
$originals = [];
|
||||
$generateds = [];
|
||||
foreach (['web', 'api'] as $type) {
|
||||
$content = file_exists("original-{$type}.txt") ? file_get_contents("original-{$type}.txt") : "";
|
||||
$array = array_values(array_filter(array_unique(explode("\n", $content))));
|
||||
$generateds[$type] = $array;
|
||||
$originals = array_merge($originals, $array);
|
||||
}
|
||||
return [$originals, $generateds];
|
||||
}
|
||||
|
||||
// ---- 公共:构建 translations 映射(normalizedKey -> obj),并收集冗余/占位符错乱 ----
|
||||
function build_translations(array $originals): array
|
||||
{
|
||||
$translations = [];
|
||||
$redundants = [];
|
||||
$regrror = [];
|
||||
if (!file_exists("translate.json")) {
|
||||
fwrite(STDERR, "translate.json not exists\n");
|
||||
exit(1);
|
||||
}
|
||||
$tmps = json_decode(file_get_contents("translate.json"), true);
|
||||
foreach ($tmps as $obj) {
|
||||
if (!isset($obj['key'])) {
|
||||
continue;
|
||||
}
|
||||
$currentKey = $obj['key'];
|
||||
$originalKey = preg_replace(["/\(%T\d+\)/", "/\(%M\d+\)/"], ["(*)", "(**)"], $currentKey);
|
||||
if (!in_array($originalKey, $originals)) {
|
||||
$redundants[$originalKey] = $obj;
|
||||
continue;
|
||||
}
|
||||
$translations[$originalKey] = $obj;
|
||||
if (preg_match_all('/\(%[TM]\d+\)/', $currentKey, $matches)) {
|
||||
foreach ($matches[0] as $match) {
|
||||
foreach ($obj as $k => $v) {
|
||||
if (empty($v)) {
|
||||
continue;
|
||||
}
|
||||
if (!str_contains($v, $match)) {
|
||||
$regrror[$originalKey] = ['key' => $currentKey, 'field' => $k, 'value' => $v, 'match' => $match];
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return [$translations, $redundants, $regrror];
|
||||
}
|
||||
|
||||
// ---- 公共:由 translate.json + originals 重新生成 public 文件 ----
|
||||
function generate(array $generateds, array $translations): void
|
||||
{
|
||||
foreach ($generateds as $type => $array) {
|
||||
$datas = [];
|
||||
foreach ($array as $text) {
|
||||
$text = trim($text);
|
||||
if (isset($translations[$text])) {
|
||||
$datas[] = $translations[$text];
|
||||
}
|
||||
}
|
||||
$inOrder = [];
|
||||
foreach ($datas as $index => $item) {
|
||||
if (preg_match('/\(%[TM]\d+\)/', $item['key'])) {
|
||||
$inOrder[$index] = strlen($item['key']);
|
||||
} else {
|
||||
$inOrder[$index] = strlen($item['key']) + 10000000000;
|
||||
}
|
||||
}
|
||||
array_multisort($inOrder, SORT_DESC, $datas);
|
||||
$results = [];
|
||||
foreach ($datas as $items) {
|
||||
foreach ($items as $kk => $item) {
|
||||
$results[$kk][] = $item;
|
||||
}
|
||||
}
|
||||
if ($type === 'api') {
|
||||
if (!is_dir("../public/language/api")) {
|
||||
mkdir("../public/language/api", 0777, true);
|
||||
}
|
||||
foreach ($results as $kk => $item) {
|
||||
file_put_contents("../public/language/api/$kk.json", json_encode($item, JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
} elseif ($type === 'web') {
|
||||
if (!is_dir("../public/language/web")) {
|
||||
mkdir("../public/language/web", 0777, true);
|
||||
}
|
||||
foreach ($results as $kk => $item) {
|
||||
file_put_contents("../public/language/web/$kk.js", "if(typeof window.LANGUAGE_DATA===\"undefined\")window.LANGUAGE_DATA={};window.LANGUAGE_DATA[\"{$kk}\"]=" . json_encode($item, JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
echo "[$type] total: " . count($results['key']) . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
if ($cmd === 'diff') {
|
||||
[$originals, $generateds] = read_generateds();
|
||||
[$translations, $redundants, $regrror] = build_translations($originals);
|
||||
|
||||
// 需要翻译的数据(对齐 translate.php 150-169:占位符按单一计数器编号)
|
||||
$needs = [];
|
||||
foreach ($originals as $text) {
|
||||
$key = trim($text);
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
if (!isset($translations[$key])) {
|
||||
$needs[$key] = $key;
|
||||
}
|
||||
}
|
||||
$needsOut = [];
|
||||
foreach ($needs as $key) {
|
||||
$c = 1;
|
||||
$converted = preg_replace_callback('/\((\*+)\)/', function ($m) use (&$c) {
|
||||
$label = strlen($m[1]) > 1 ? "M" : "T";
|
||||
return "(%" . $label . $c++ . ")";
|
||||
}, $key);
|
||||
$needsOut[] = ['key' => $converted];
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'needsCount' => count($needsOut),
|
||||
'redundantCount' => count($redundants),
|
||||
'regexErrorCount' => count($regrror),
|
||||
'needs' => $needsOut,
|
||||
'redundants' => array_keys($redundants),
|
||||
'regexErrors' => array_values($regrror),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
|
||||
|
||||
if (count($regrror) > 0) {
|
||||
exit(2); // 已有数据占位符错乱,需先修复
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if ($cmd === 'apply') {
|
||||
$file = $argv[2] ?? '';
|
||||
if ($file === '' || !file_exists($file)) {
|
||||
fwrite(STDERR, "用法:apply <translated.json>(文件不存在)\n");
|
||||
exit(1);
|
||||
}
|
||||
[$originals, $generateds] = read_generateds();
|
||||
[$translations, $redundants, $regrror] = build_translations($originals);
|
||||
if (count($regrror) > 0) {
|
||||
fwrite(STDERR, "translate.json 已有条目占位符错乱,请先修复再发版。\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
$incoming = json_decode(file_get_contents($file), true);
|
||||
if (!is_array($incoming)) {
|
||||
fwrite(STDERR, "translated.json 必须是数组\n");
|
||||
exit(1);
|
||||
}
|
||||
$added = 0;
|
||||
foreach ($incoming as $raw) {
|
||||
foreach ($GLOBALS['LANG_FIELDS'] as $f) {
|
||||
if (!array_key_exists($f, $raw)) {
|
||||
fwrite(STDERR, "新翻译缺字段 \"$f\":" . json_encode($raw, JSON_UNESCAPED_UNICODE) . "\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
// 占位符完整性:key 里每个 (%T1)/(%M1) 必须出现在每个非空语言值里
|
||||
if (preg_match_all('/\(%[TM]\d+\)/', $raw['key'], $m)) {
|
||||
foreach ($m[0] as $match) {
|
||||
foreach ($GLOBALS['LANG_FIELDS'] as $f) {
|
||||
if ($f === 'key' || $f === 'zh') {
|
||||
continue;
|
||||
}
|
||||
if (empty($raw[$f])) {
|
||||
continue;
|
||||
}
|
||||
if (!str_contains($raw[$f], $match)) {
|
||||
fwrite(STDERR, "占位符 $match 在字段 \"$f\" 缺失:{$raw['key']}\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 规范化:固定字段顺序 + zh 置空
|
||||
$item = [];
|
||||
foreach ($GLOBALS['LANG_FIELDS'] as $f) {
|
||||
$item[$f] = $f === 'zh' ? '' : $raw[$f];
|
||||
}
|
||||
$originalKey = preg_replace(["/\(%T\d+\)/", "/\(%M\d+\)/"], ["(*)", "(**)"], $item['key']);
|
||||
$translations[$originalKey] = $item;
|
||||
$added++;
|
||||
}
|
||||
|
||||
// array_values:现有条目(去冗余)在前,新条目追加在后
|
||||
file_put_contents("translate.json", json_encode(array_values($translations), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||||
echo json_encode([
|
||||
'added' => $added,
|
||||
'total' => count($translations),
|
||||
'droppedRedundant' => count($redundants),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if ($cmd === 'generate') {
|
||||
[$originals, $generateds] = read_generateds();
|
||||
[$translations] = build_translations($originals);
|
||||
generate($generateds, $translations);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
fwrite(STDERR, "未知子命令:'$cmd'。可用:diff | apply <file> | generate\n");
|
||||
exit(1);
|
||||
47
.claude/skills/dootask-release/scripts/version_bump.js
vendored
Normal file
47
.claude/skills/dootask-release/scripts/version_bump.js
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env node
|
||||
// 计算并写入新版本号到 package.json(version + codeVerson),算法对齐 bin/version.js。
|
||||
// 不生成 CHANGELOG(在技能流程内撰写),只输出版本号与 changelog 的提交区间。
|
||||
//
|
||||
// 项目根相对脚本自身定位(脚本固定在 <root>/.claude/skills/dootask-release/scripts/),与调用时的 cwd 无关。
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../../../..');
|
||||
const pkgFile = path.join(ROOT, 'package.json');
|
||||
const verOffset = 6394; // 版本号偏移量(与 bin/version.js 一致)
|
||||
const codeOffset = 35; // 代码版本号偏移量
|
||||
|
||||
function git(cmd) {
|
||||
return execSync(cmd, { cwd: ROOT, maxBuffer: 1024 * 1024 * 10 }).toString().trim();
|
||||
}
|
||||
|
||||
const verCount = parseInt(git('git rev-list --count HEAD'), 10);
|
||||
const codeCount = parseInt(git("git tag --merged pro -l 'v*' | wc -l"), 10);
|
||||
const num = verOffset + verCount;
|
||||
if (Number.isNaN(num)) {
|
||||
console.error(`版本计算失败:rev-list count=${verCount}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const version = `${Math.floor(num / 10000)}.${Math.floor((num % 10000) / 100)}.${Math.floor(num % 100)}`;
|
||||
const codeVersion = codeOffset + codeCount;
|
||||
|
||||
let pkg = fs.readFileSync(pkgFile, 'utf8');
|
||||
const prevVersion = (pkg.match(/"version":\s*"(.*?)"/) || [])[1] || '';
|
||||
pkg = pkg.replace(/"version":\s*"(.*?)"/, `"version": "${version}"`);
|
||||
pkg = pkg.replace(/"codeVerson":(.*?)(,|$)/, `"codeVerson": ${codeVersion}$2`);
|
||||
fs.writeFileSync(pkgFile, pkg, 'utf8');
|
||||
|
||||
// 上一个 release 提交作为 changelog 区间下界
|
||||
let prevReleaseCommit = '';
|
||||
try {
|
||||
prevReleaseCommit = git("git log --grep='^release: v' -n 1 --pretty=format:%H");
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
console.log(JSON.stringify({
|
||||
version,
|
||||
codeVersion,
|
||||
prevVersion,
|
||||
prevReleaseCommit,
|
||||
changelogRange: prevReleaseCommit ? `${prevReleaseCommit}..HEAD` : '(未找到上一个 release 提交,需人工确定区间)',
|
||||
}, null, 2));
|
||||
Loading…
x
Reference in New Issue
Block a user