siluzan-tso-cli 1.1.15-beta.1 → 1.1.15-beta.2
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 +1 -1
- package/dist/index.js +279 -83
- package/dist/skill/SKILL.md +2 -0
- package/dist/skill/_meta.json +2 -2
- package/dist/skill/references/write-audit-restore.md +76 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
|
|
|
51
51
|
siluzan-tso init --force # 强制覆盖已存在文件
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
> **注意**:当前为测试版(1.1.15-beta.
|
|
54
|
+
> **注意**:当前为测试版(1.1.15-beta.2),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
|
|
55
55
|
|
|
56
56
|
| 助手 | 建议 `--ai` |
|
|
57
57
|
| ----------------------- | ------------------------------------ |
|
package/dist/index.js
CHANGED
|
@@ -2760,6 +2760,8 @@ async function fetchLatestVersion() {
|
|
|
2760
2760
|
import * as path5 from "path";
|
|
2761
2761
|
import { mkdir as mkdir2, writeFile, chmod as chmod2 } from "fs/promises";
|
|
2762
2762
|
var MAX_SNAPSHOT_BYTES = 2 * 1024 * 1024;
|
|
2763
|
+
var CAMPAIGN_DETAIL_RE = /^\/campaignmanagement\/campaign\/[^/]+\/[^/]+\/?$/i;
|
|
2764
|
+
var SEM_MANAGEMENT_RE = /\/SemManagement\//i;
|
|
2763
2765
|
function shouldAttemptPreSnapshot(method, url) {
|
|
2764
2766
|
const m = method.toUpperCase();
|
|
2765
2767
|
if (m !== "PUT" && m !== "PATCH") return false;
|
|
@@ -2771,7 +2773,9 @@ function shouldAttemptPreSnapshot(method, url) {
|
|
|
2771
2773
|
}
|
|
2772
2774
|
const host = u.hostname.toLowerCase();
|
|
2773
2775
|
if (!host.includes("googleapi")) return false;
|
|
2774
|
-
|
|
2776
|
+
if (SEM_MANAGEMENT_RE.test(u.pathname)) return true;
|
|
2777
|
+
if (CAMPAIGN_DETAIL_RE.test(u.pathname)) return true;
|
|
2778
|
+
return false;
|
|
2775
2779
|
}
|
|
2776
2780
|
function getSnapshotGetUrl(method, url) {
|
|
2777
2781
|
if (!shouldAttemptPreSnapshot(method, url)) return null;
|
|
@@ -3671,6 +3675,10 @@ function applyListFilters(records, opts) {
|
|
|
3671
3675
|
});
|
|
3672
3676
|
}
|
|
3673
3677
|
}
|
|
3678
|
+
const rk = opts.resourceKey?.trim();
|
|
3679
|
+
if (rk) {
|
|
3680
|
+
out = out.filter((r) => r.resourceKey === rk);
|
|
3681
|
+
}
|
|
3674
3682
|
return out;
|
|
3675
3683
|
}
|
|
3676
3684
|
async function runAuditList(opts) {
|
|
@@ -3690,6 +3698,7 @@ async function runAuditList(opts) {
|
|
|
3690
3698
|
outcome: opts.outcome,
|
|
3691
3699
|
runId: opts.runId,
|
|
3692
3700
|
sinceTs: opts.sinceTs,
|
|
3701
|
+
resourceKey: opts.resourceKey,
|
|
3693
3702
|
failuresOnly: Boolean(opts.failuresOnly)
|
|
3694
3703
|
},
|
|
3695
3704
|
items: filtered
|
|
@@ -3809,77 +3818,178 @@ auditId: ${record.auditId ?? "\u2014"}`);
|
|
|
3809
3818
|
// src/commands/audit-restore.ts
|
|
3810
3819
|
import { createHash } from "crypto";
|
|
3811
3820
|
var TSO_WRITE_AUDIT_NS3 = "tso";
|
|
3821
|
+
var SUBSEQUENT_WRITES_LOOKBACK_DAYS_MAX = 400;
|
|
3812
3822
|
async function loadSnapshotForRecord(record) {
|
|
3813
3823
|
if (!record.preSnapshotRef) return null;
|
|
3814
3824
|
return readPreSnapshotPayload(TSO_WRITE_AUDIT_NS3, record.preSnapshotRef);
|
|
3815
3825
|
}
|
|
3816
|
-
|
|
3817
|
-
const
|
|
3818
|
-
if (
|
|
3819
|
-
|
|
3820
|
-
|
|
3826
|
+
function beijingDayFromIsoTs(ts) {
|
|
3827
|
+
const d = new Date(ts);
|
|
3828
|
+
if (Number.isNaN(d.getTime())) return formatBeijingYmd(/* @__PURE__ */ new Date());
|
|
3829
|
+
return formatBeijingYmd(d);
|
|
3830
|
+
}
|
|
3831
|
+
async function collectSubsequentWrites(target) {
|
|
3832
|
+
const key = target.resourceKey?.trim();
|
|
3833
|
+
if (!key) return [];
|
|
3834
|
+
const sinceDay = beijingDayFromIsoTs(target.ts);
|
|
3835
|
+
const safeSince = sinceDay && /^\d{4}-\d{2}-\d{2}$/.test(sinceDay) ? sinceDay : beijingDayStringDaysAgo(SUBSEQUENT_WRITES_LOOKBACK_DAYS_MAX);
|
|
3836
|
+
const records = await readWriteAuditRecordsSince(TSO_WRITE_AUDIT_NS3, safeSince);
|
|
3837
|
+
const targetMs = new Date(target.ts).getTime();
|
|
3838
|
+
return records.filter((r) => {
|
|
3839
|
+
if (r.resourceKey !== key) return false;
|
|
3840
|
+
if (r.auditId === target.auditId) return false;
|
|
3841
|
+
const t = new Date(r.ts).getTime();
|
|
3842
|
+
if (Number.isNaN(t) || Number.isNaN(targetMs)) return false;
|
|
3843
|
+
return t > targetMs;
|
|
3844
|
+
}).map((r) => ({
|
|
3845
|
+
auditId: r.auditId ?? "",
|
|
3846
|
+
ts: r.ts,
|
|
3847
|
+
method: r.method,
|
|
3848
|
+
outcome: r.outcome,
|
|
3849
|
+
pathname: r.pathname,
|
|
3850
|
+
commit: r.commit,
|
|
3851
|
+
invokedCommand: r.invokedCommand,
|
|
3852
|
+
hasSnapshot: Boolean(r.preSnapshotRef)
|
|
3853
|
+
})).sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
|
|
3854
|
+
}
|
|
3855
|
+
function deriveSupportability(record, snapshot) {
|
|
3856
|
+
if (!record.auditId) {
|
|
3857
|
+
return {
|
|
3858
|
+
code: "v1_record_no_audit_id",
|
|
3859
|
+
reason: "v1 \u5386\u53F2\u5BA1\u8BA1\u884C\u7F3A auditId/preSnapshotRef\uFF0C\u65E0\u6CD5\u7528 restore-apply",
|
|
3860
|
+
hint: "\u8BF7\u4EBA\u5DE5\u6839\u636E commit \u6587\u672C\u4E0E\u539F\u59CB\u53D8\u66F4\u4EBA\u5DE5\u6062\u590D\uFF1B\u6216\u4EC5\u4F5C\u4E3A\u53C2\u8003\u67E5\u770B audit show"
|
|
3861
|
+
};
|
|
3862
|
+
}
|
|
3863
|
+
if (record.outcome !== "success") {
|
|
3864
|
+
return {
|
|
3865
|
+
code: "not_success_write",
|
|
3866
|
+
reason: `audit \u7ED3\u679C\u4E3A ${record.outcome}\uFF0C\u539F\u5199\u672A\u751F\u6548\uFF0C\u65E0\u9700\u8865\u507F`,
|
|
3867
|
+
hint: "\u5931\u8D25\u7684\u5199\u4E0D\u4F1A\u6539\u53D8\u7EBF\u4E0A\u72B6\u6001\uFF1B\u5982\u9700\u6392\u67E5\u8BF7\u770B audit show \u4E2D\u7684 error \u5B57\u6BB5"
|
|
3868
|
+
};
|
|
3869
|
+
}
|
|
3870
|
+
const method = record.method.toUpperCase();
|
|
3871
|
+
if (method === "POST") {
|
|
3872
|
+
return {
|
|
3873
|
+
code: "unsupported_method_post",
|
|
3874
|
+
reason: "POST \u521B\u5EFA\u578B\u5199\u65E0\u5199\u524D\u5FEB\u7167\uFF0C\u65E0\u6CD5\u7528\u540C URL \u8865\u507F",
|
|
3875
|
+
hint: "POST \u64A4\u9500\u9700\u8C03\u7528\u5BF9\u5E94\u8D44\u6E90\u7684 DELETE \u7AEF\u70B9\uFF08CLI \u6682\u672A\u81EA\u52A8\u6D3E\u751F\uFF09\uFF1B\u8BF7\u4EBA\u5DE5\u8FD0\u884C\u5BF9\u5E94 delete/stop \u547D\u4EE4"
|
|
3876
|
+
};
|
|
3877
|
+
}
|
|
3878
|
+
if (method === "DELETE") {
|
|
3879
|
+
return {
|
|
3880
|
+
code: "unsupported_method_delete",
|
|
3881
|
+
reason: "DELETE \u5220\u9664\u578B\u5199\u5728\u5F53\u524D\u5FEB\u7167\u767D\u540D\u5355\u5916\uFF0C\u65E0\u5199\u524D GET",
|
|
3882
|
+
hint: "DELETE \u64A4\u9500\u9700\u6839\u636E\u5386\u53F2\u5FEB\u7167\u91CD\u5EFA\u8D44\u6E90\uFF1B\u5F53\u524D CLI \u672A\u5BF9\u8BE5\u8DEF\u5F84\u914D\u7F6E\u5199\u524D\u5FEB\u7167\uFF0C\u8BF7\u4EBA\u5DE5\u6062\u590D"
|
|
3883
|
+
};
|
|
3884
|
+
}
|
|
3885
|
+
if (method !== "PUT" && method !== "PATCH") {
|
|
3886
|
+
return {
|
|
3887
|
+
code: "unsupported_method_other",
|
|
3888
|
+
reason: `\u8865\u507F\u5199\u4EC5\u652F\u6301 PUT/PATCH\uFF0C\u672C\u6761\u4E3A ${method}`
|
|
3889
|
+
};
|
|
3890
|
+
}
|
|
3891
|
+
if (!record.preSnapshotRef) {
|
|
3892
|
+
return {
|
|
3893
|
+
code: "no_snapshot",
|
|
3894
|
+
reason: "\u8BE5 PUT/PATCH \u672A\u547D\u4E2D\u5199\u524D\u5FEB\u7167\u767D\u540D\u5355\uFF08\u4EC5 Google SemManagement \u9ED8\u8BA4\u5F00\u542F\uFF09",
|
|
3895
|
+
hint: "\u65E0\u6CD5\u81EA\u52A8\u8865\u507F\uFF1B\u8BF7\u4EBA\u5DE5 GET \u5386\u53F2\u503C\u540E\u901A\u8FC7\u5E38\u89C4 ad/optimize \u547D\u4EE4\u53CD\u5411\u5199"
|
|
3896
|
+
};
|
|
3897
|
+
}
|
|
3898
|
+
if (!snapshot) {
|
|
3899
|
+
return {
|
|
3900
|
+
code: "snapshot_missing_or_invalid",
|
|
3901
|
+
reason: "preSnapshotRef \u6307\u5411\u7684 snapshot JSON \u4E0D\u53EF\u8BFB\u6216\u8D8A\u754C",
|
|
3902
|
+
hint: "\u68C0\u67E5 ~/.siluzan/write-audit/tso/snapshots/<auditId>.json \u662F\u5426\u5B58\u5728\uFF1B\u6216\u4FDD\u7559\u671F\u5DF2\u8FC7\u88AB\u6E05\u7406"
|
|
3903
|
+
};
|
|
3821
3904
|
}
|
|
3905
|
+
return {
|
|
3906
|
+
code: "ready_put_patch",
|
|
3907
|
+
reason: "PUT/PATCH \u5355\u6B65\u8865\u507F\u5199\u5C31\u7EEA"
|
|
3908
|
+
};
|
|
3909
|
+
}
|
|
3910
|
+
async function buildRestoreContext(auditId) {
|
|
3822
3911
|
const record = await findWriteAuditRecordByAuditId(TSO_WRITE_AUDIT_NS3, auditId);
|
|
3823
|
-
let plan;
|
|
3824
3912
|
if (!record) {
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3913
|
+
return {
|
|
3914
|
+
record: null,
|
|
3915
|
+
snapshot: null,
|
|
3916
|
+
subsequentWrites: [],
|
|
3917
|
+
successfulSubsequentWriteCount: 0,
|
|
3918
|
+
supportability: {
|
|
3919
|
+
code: "audit_not_found",
|
|
3920
|
+
reason: `\u672C\u673A\u6700\u8FD1 ${SUBSEQUENT_WRITES_LOOKBACK_DAYS_MAX} \u4E2A\u5317\u4EAC\u65E5\u5185\u672A\u627E\u5230 auditId=${auditId}`,
|
|
3921
|
+
hint: "\u7528 audit list --days 14 --match <commit/path \u7247\u6BB5> \u53CD\u67E5\uFF1B\u8D85\u8FC7\u4FDD\u7559\u671F\u53EF\u80FD\u5DF2\u88AB\u6E05\u7406"
|
|
3922
|
+
}
|
|
3835
3923
|
};
|
|
3836
|
-
}
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3924
|
+
}
|
|
3925
|
+
const snapshot = await loadSnapshotForRecord(record);
|
|
3926
|
+
const subsequentWrites = await collectSubsequentWrites(record);
|
|
3927
|
+
const successfulSubsequentWriteCount = subsequentWrites.filter(
|
|
3928
|
+
(w) => w.outcome === "success"
|
|
3929
|
+
).length;
|
|
3930
|
+
return {
|
|
3931
|
+
record,
|
|
3932
|
+
snapshot,
|
|
3933
|
+
resourceKey: record.resourceKey,
|
|
3934
|
+
subsequentWrites,
|
|
3935
|
+
successfulSubsequentWriteCount,
|
|
3936
|
+
supportability: deriveSupportability(record, snapshot)
|
|
3937
|
+
};
|
|
3938
|
+
}
|
|
3939
|
+
function ctxToPlanPayload(auditId, ctx) {
|
|
3940
|
+
const { record, snapshot, subsequentWrites, successfulSubsequentWriteCount, supportability } = ctx;
|
|
3941
|
+
const target = record ? {
|
|
3942
|
+
ts: record.ts,
|
|
3943
|
+
pathname: record.pathname,
|
|
3944
|
+
method: record.method,
|
|
3945
|
+
outcome: record.outcome,
|
|
3946
|
+
commit: record.commit,
|
|
3947
|
+
invokedCommand: record.invokedCommand,
|
|
3948
|
+
httpStatus: record.httpStatus
|
|
3949
|
+
} : null;
|
|
3950
|
+
const snapshotSummary = snapshot ? (() => {
|
|
3951
|
+
const bodyJson = JSON.stringify(snapshot.body);
|
|
3952
|
+
return {
|
|
3953
|
+
capturedAt: snapshot.capturedAt,
|
|
3954
|
+
originalMethod: snapshot.originalMethod,
|
|
3955
|
+
originalUrl: snapshot.originalUrl,
|
|
3956
|
+
bodyUtf8Length: Buffer.byteLength(bodyJson, "utf8"),
|
|
3957
|
+
bodySha256Prefix: createHash("sha256").update(bodyJson).digest("hex").slice(0, 16)
|
|
3958
|
+
};
|
|
3959
|
+
})() : null;
|
|
3960
|
+
const steps = supportability.code === "ready_put_patch" && snapshot ? [
|
|
3961
|
+
(() => {
|
|
3962
|
+
const bodyJson = JSON.stringify(snapshot.body);
|
|
3963
|
+
return {
|
|
3964
|
+
order: 1,
|
|
3965
|
+
method: snapshot.originalMethod.toUpperCase(),
|
|
3966
|
+
url: snapshot.originalUrl,
|
|
3967
|
+
body: snapshot.body,
|
|
3968
|
+
bodyUtf8Length: Buffer.byteLength(bodyJson, "utf8"),
|
|
3969
|
+
bodySha256Prefix: createHash("sha256").update(bodyJson).digest("hex").slice(0, 16)
|
|
3844
3970
|
};
|
|
3845
|
-
}
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
pathname: record.pathname,
|
|
3865
|
-
commit: record.commit ?? "",
|
|
3866
|
-
method: record.method,
|
|
3867
|
-
outcome: record.outcome
|
|
3868
|
-
},
|
|
3869
|
-
steps: [
|
|
3870
|
-
{
|
|
3871
|
-
order: 1,
|
|
3872
|
-
method: m,
|
|
3873
|
-
url: snap.originalUrl,
|
|
3874
|
-
body: snap.body,
|
|
3875
|
-
bodyUtf8Length: Buffer.byteLength(bodyJson, "utf8"),
|
|
3876
|
-
bodySha256Prefix: hash
|
|
3877
|
-
}
|
|
3878
|
-
]
|
|
3879
|
-
};
|
|
3880
|
-
}
|
|
3881
|
-
}
|
|
3971
|
+
})()
|
|
3972
|
+
] : [];
|
|
3973
|
+
return {
|
|
3974
|
+
auditId,
|
|
3975
|
+
resourceKey: ctx.resourceKey,
|
|
3976
|
+
supportability,
|
|
3977
|
+
willMutate: supportability.code === "ready_put_patch",
|
|
3978
|
+
target,
|
|
3979
|
+
snapshot: snapshotSummary,
|
|
3980
|
+
subsequentWrites,
|
|
3981
|
+
guardChecks: { ackSubsequentWrites: successfulSubsequentWriteCount },
|
|
3982
|
+
steps
|
|
3983
|
+
};
|
|
3984
|
+
}
|
|
3985
|
+
async function runAuditRestorePlan(opts) {
|
|
3986
|
+
const auditId = opts.id?.trim() ?? "";
|
|
3987
|
+
if (!auditId) {
|
|
3988
|
+
console.error("\u8BF7\u4F7F\u7528 --id <auditId>");
|
|
3989
|
+
process.exit(1);
|
|
3882
3990
|
}
|
|
3991
|
+
const ctx = await buildRestoreContext(auditId);
|
|
3992
|
+
const plan = ctxToPlanPayload(auditId, ctx);
|
|
3883
3993
|
const envelope = {
|
|
3884
3994
|
auditDir: getWriteAuditRootDir(TSO_WRITE_AUDIT_NS3),
|
|
3885
3995
|
plan
|
|
@@ -3890,15 +4000,62 @@ async function runAuditRestorePlan(opts) {
|
|
|
3890
4000
|
)) {
|
|
3891
4001
|
return;
|
|
3892
4002
|
}
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
4003
|
+
printPlanForHuman(envelope);
|
|
4004
|
+
}
|
|
4005
|
+
function printPlanForHuman(envelope) {
|
|
4006
|
+
const { plan } = envelope;
|
|
4007
|
+
console.log(`
|
|
4008
|
+
[restore-plan] auditDir: ${envelope.auditDir}`);
|
|
4009
|
+
console.log(`auditId: ${plan.auditId}`);
|
|
4010
|
+
if (plan.resourceKey) console.log(`resourceKey: ${plan.resourceKey}`);
|
|
4011
|
+
console.log(`supportability: ${plan.supportability.code} \u2014\u2014 ${plan.supportability.reason}`);
|
|
4012
|
+
if (plan.supportability.hint) console.log(` hint: ${plan.supportability.hint}`);
|
|
4013
|
+
if (plan.target) {
|
|
4014
|
+
console.log(
|
|
4015
|
+
`
|
|
4016
|
+
\u76EE\u6807 audit\uFF1A${plan.target.ts} ${plan.target.method} ${plan.target.pathname} outcome=${plan.target.outcome}` + (plan.target.commit ? `
|
|
4017
|
+
commit: ${plan.target.commit}` : "")
|
|
4018
|
+
);
|
|
4019
|
+
}
|
|
4020
|
+
if (plan.snapshot) {
|
|
4021
|
+
console.log(
|
|
4022
|
+
`
|
|
4023
|
+
\u5199\u524D\u5FEB\u7167\uFF1AcapturedAt=${plan.snapshot.capturedAt} originalMethod=${plan.snapshot.originalMethod}
|
|
4024
|
+
url: ${plan.snapshot.originalUrl}
|
|
4025
|
+
body: ${plan.snapshot.bodyUtf8Length} bytes sha256[0..16]=${plan.snapshot.bodySha256Prefix}`
|
|
4026
|
+
);
|
|
4027
|
+
}
|
|
4028
|
+
console.log(
|
|
4029
|
+
`
|
|
4030
|
+
\u540C\u8D44\u6E90\u540E\u7EED\u5199\uFF08target.ts \u4E4B\u540E\uFF0C\u542B\u5931\u8D25\uFF09\uFF1A${plan.subsequentWrites.length} \u6761\uFF1B\u5176\u4E2D\u6210\u529F ${plan.guardChecks.ackSubsequentWrites} \u6761`
|
|
4031
|
+
);
|
|
4032
|
+
if (plan.subsequentWrites.length > 0) {
|
|
4033
|
+
console.log(
|
|
4034
|
+
` \u26A0\uFE0F \u6267\u884C restore-apply \u4F1A\u628A snapshot.body \u6574\u4E2A PUT/PATCH \u56DE\u53BB\uFF0C\u6210\u529F\u7684 ${plan.guardChecks.ackSubsequentWrites} \u6761\u540E\u7EED\u5199\u4F1A\u88AB\u8986\u76D6\u3002`
|
|
4035
|
+
);
|
|
4036
|
+
for (const w of plan.subsequentWrites.slice(0, 10)) {
|
|
4037
|
+
console.log(
|
|
4038
|
+
` - ${w.ts} ${w.method} outcome=${w.outcome} auditId=${w.auditId}` + (w.commit ? ` commit=${w.commit.slice(0, 60)}` : "")
|
|
4039
|
+
);
|
|
4040
|
+
}
|
|
4041
|
+
if (plan.subsequentWrites.length > 10) {
|
|
4042
|
+
console.log(` \u2026\u2026 \u8FD8\u6709 ${plan.subsequentWrites.length - 10} \u6761\uFF0C\u8BF7\u8FD0\u884C audit list --resource-key "${plan.resourceKey ?? ""}" --json \u67E5\u770B\u5B8C\u6574\u5386\u53F2`);
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
if (plan.steps.length > 0) {
|
|
4046
|
+
console.log(`
|
|
4047
|
+
\u8865\u507F\u5199\u6B65\u9AA4\uFF08\u4EC5\u5C55\u793A\uFF0C\u672A\u4FEE\u6539\u7EBF\u4E0A\u8D44\u6E90\uFF09\uFF1A`);
|
|
4048
|
+
console.log(JSON.stringify(plan.steps, null, 2));
|
|
3896
4049
|
console.log(
|
|
3897
|
-
|
|
4050
|
+
`
|
|
4051
|
+
\u786E\u8BA4\u65E0\u8BEF\u540E\u6267\u884C\uFF1A
|
|
4052
|
+
siluzan-tso audit restore-apply --id ${plan.auditId} --i-confirm --ack-subsequent-writes ${plan.guardChecks.ackSubsequentWrites} [--commit "\u56DE\u9000\u8BF4\u660E"]
|
|
4053
|
+
`
|
|
3898
4054
|
);
|
|
3899
4055
|
} else {
|
|
3900
|
-
console.log(
|
|
3901
|
-
|
|
4056
|
+
console.log(`
|
|
4057
|
+
\u5F53\u524D supportability=${plan.supportability.code}\uFF0C\u65E0\u53EF\u6267\u884C step\u3002
|
|
4058
|
+
`);
|
|
3902
4059
|
}
|
|
3903
4060
|
}
|
|
3904
4061
|
async function runAuditRestoreApply(opts) {
|
|
@@ -3913,19 +4070,41 @@ async function runAuditRestoreApply(opts) {
|
|
|
3913
4070
|
console.error("\u8BF7\u4F7F\u7528 --id <auditId>");
|
|
3914
4071
|
process.exit(1);
|
|
3915
4072
|
}
|
|
3916
|
-
const
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
4073
|
+
const ctx = await buildRestoreContext(auditId);
|
|
4074
|
+
if (ctx.supportability.code !== "ready_put_patch") {
|
|
4075
|
+
console.error(
|
|
4076
|
+
`
|
|
4077
|
+
\u274C \u65E0\u6CD5\u6267\u884C\uFF1Asupportability=${ctx.supportability.code}
|
|
4078
|
+
\u539F\u56E0\uFF1A${ctx.supportability.reason}` + (ctx.supportability.hint ? `
|
|
4079
|
+
\u5EFA\u8BAE\uFF1A${ctx.supportability.hint}` : "") + `
|
|
4080
|
+
\u8BF7\u5148 siluzan-tso audit restore-plan --id ${auditId} \u67E5\u770B\u5B8C\u6574\u8BCA\u65AD\u3002
|
|
4081
|
+
`
|
|
4082
|
+
);
|
|
3920
4083
|
process.exit(1);
|
|
3921
4084
|
}
|
|
3922
|
-
const
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
`
|
|
4085
|
+
const expected = ctx.successfulSubsequentWriteCount;
|
|
4086
|
+
const ack = Number.isInteger(opts.ackSubsequentWrites) ? Number(opts.ackSubsequentWrites) : -1;
|
|
4087
|
+
if (ack < 0) {
|
|
4088
|
+
console.error(
|
|
4089
|
+
`
|
|
4090
|
+
\u274C \u62D2\u7EDD\u6267\u884C\uFF1A\u5FC5\u987B\u663E\u5F0F\u786E\u8BA4\u4F1A\u88AB\u8986\u76D6\u7684"\u540C\u8D44\u6E90\u540E\u7EED\u6210\u529F\u5199"\u6570\u91CF\u3002
|
|
4091
|
+
\u5F53\u524D\u626B\u63CF\u7ED3\u679C\uFF1A${expected} \u6761\uFF1B\u8BF7\u8FFD\u52A0\uFF1A--ack-subsequent-writes ${expected}
|
|
4092
|
+
\u82E5\u8BE5\u6570\u91CF\u4E0D\u4E3A 0\uFF0C\u610F\u5473\u7740\u6709\u66F4\u665A\u7684\u6210\u529F\u5199\u4F1A\u88AB\u672C\u6B21\u56DE\u9000\u8986\u76D6\uFF1B\u8BF7\u5148 restore-plan \u590D\u6838\u3002
|
|
4093
|
+
`
|
|
4094
|
+
);
|
|
3927
4095
|
process.exit(1);
|
|
3928
4096
|
}
|
|
4097
|
+
if (ack !== expected) {
|
|
4098
|
+
console.error(
|
|
4099
|
+
`
|
|
4100
|
+
\u274C \u62D2\u7EDD\u6267\u884C\uFF1A--ack-subsequent-writes=${ack} \u4E0E\u5F53\u524D\u626B\u63CF\u7ED3\u679C ${expected} \u4E0D\u4E00\u81F4\u3002
|
|
4101
|
+
\u8BF4\u660E plan \u4E0E apply \u4E4B\u95F4\u53C8\u53D1\u751F\u4E86\u65B0\u5199\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C restore-plan \u590D\u6838\u540E\u518D apply\u3002
|
|
4102
|
+
`
|
|
4103
|
+
);
|
|
4104
|
+
process.exit(1);
|
|
4105
|
+
}
|
|
4106
|
+
const snap = ctx.snapshot;
|
|
4107
|
+
const method = snap.originalMethod.toUpperCase();
|
|
3929
4108
|
if (getResolvedWriteAuditCommit().trim() === "") {
|
|
3930
4109
|
setWriteAuditCliCommit(`compensate:restore:${auditId}`);
|
|
3931
4110
|
}
|
|
@@ -3933,15 +4112,20 @@ async function runAuditRestoreApply(opts) {
|
|
|
3933
4112
|
await apiFetch2(
|
|
3934
4113
|
snap.originalUrl,
|
|
3935
4114
|
config,
|
|
3936
|
-
{ method
|
|
4115
|
+
{ method, body: JSON.stringify(snap.body) },
|
|
3937
4116
|
Boolean(opts.verbose)
|
|
3938
4117
|
);
|
|
3939
|
-
const out = {
|
|
4118
|
+
const out = {
|
|
4119
|
+
ok: true,
|
|
4120
|
+
auditId,
|
|
4121
|
+
overwrittenSubsequentSuccessfulWrites: expected,
|
|
4122
|
+
message: "\u8865\u507F\u5199\u5DF2\u63D0\u4EA4\uFF08\u5E76\u8BB0\u5165\u65B0\u7684\u5BA1\u8BA1\u884C\uFF09\u3002"
|
|
4123
|
+
};
|
|
3940
4124
|
if (opts.json) {
|
|
3941
4125
|
console.log(JSON.stringify(out, null, 2));
|
|
3942
4126
|
} else {
|
|
3943
4127
|
console.log(`
|
|
3944
|
-
${out.message}
|
|
4128
|
+
${out.message}\uFF08\u5DF2\u8986\u76D6 ${expected} \u6761\u540E\u7EED\u6210\u529F\u5199\uFF09
|
|
3945
4129
|
`);
|
|
3946
4130
|
}
|
|
3947
4131
|
}
|
|
@@ -14103,7 +14287,11 @@ auditCmd.command("list").description("\u5217\u51FA\u6700\u8FD1 N \u4E2A\u5317\u4
|
|
|
14103
14287
|
"--json-out <path>",
|
|
14104
14288
|
"\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest.json\uFF08\u4E0E --json \u4E92\u65A5\uFF09\uFF1Bstdout \u4E00\u884C\u6458\u8981 JSON\uFF0C\u542B outlineFile\uFF08TS \u5F0F\u7C7B\u578B\u5728 `*.outline.txt`\uFF09",
|
|
14105
14289
|
void 0
|
|
14106
|
-
).option("--failures-only", "\u4EC5\u5931\u8D25\u8BB0\u5F55", false).option("--match <sub>", "\u5B50\u4E32\u5339\u914D commit/pathname/cmd/url/auditId/runId\uFF08\u626B JSONL\uFF09", void 0).option("--method <verb>", "HTTP \u65B9\u6CD5\u8FC7\u6EE4\uFF0C\u5982 PUT\u3001POST", void 0).option("--outcome <success|failure>", "\u6309\u7ED3\u679C\u8FC7\u6EE4", void 0).option("--run-id <id>", "\u6309 runId \u5B50\u4E32\u8FC7\u6EE4\uFF08\u73AF\u5883\u53D8\u91CF SILUZAN_AUDIT_RUN_ID \u5199\u5165\u7684\u8BB0\u5F55\uFF09", void 0).option("--since-ts <iso>", "\u4EC5 ts \u4E0D\u65E9\u4E8E\u8BE5 ISO \u65F6\u95F4\uFF08\u5728 --days \u7A97\u53E3\u5185\u518D\u7B5B\uFF09", void 0).
|
|
14290
|
+
).option("--failures-only", "\u4EC5\u5931\u8D25\u8BB0\u5F55", false).option("--match <sub>", "\u5B50\u4E32\u5339\u914D commit/pathname/cmd/url/auditId/runId\uFF08\u626B JSONL\uFF09", void 0).option("--method <verb>", "HTTP \u65B9\u6CD5\u8FC7\u6EE4\uFF0C\u5982 PUT\u3001POST", void 0).option("--outcome <success|failure>", "\u6309\u7ED3\u679C\u8FC7\u6EE4", void 0).option("--run-id <id>", "\u6309 runId \u5B50\u4E32\u8FC7\u6EE4\uFF08\u73AF\u5883\u53D8\u91CF SILUZAN_AUDIT_RUN_ID \u5199\u5165\u7684\u8BB0\u5F55\uFF09", void 0).option("--since-ts <iso>", "\u4EC5 ts \u4E0D\u65E9\u4E8E\u8BE5 ISO \u65F6\u95F4\uFF08\u5728 --days \u7A97\u53E3\u5185\u518D\u7B5B\uFF09", void 0).option(
|
|
14291
|
+
"--resource-key <key>",
|
|
14292
|
+
"\u7CBE\u786E\u5339\u914D resourceKey\uFF08host+pathname\uFF09\uFF0C\u7528\u4E8E restore-plan \u914D\u5957\u8FFD\u6EAF\u540C\u8D44\u6E90\u5168\u91CF\u5386\u53F2",
|
|
14293
|
+
void 0
|
|
14294
|
+
).action(
|
|
14107
14295
|
async (opts) => {
|
|
14108
14296
|
await runAuditList({
|
|
14109
14297
|
days: opts.days,
|
|
@@ -14114,7 +14302,8 @@ auditCmd.command("list").description("\u5217\u51FA\u6700\u8FD1 N \u4E2A\u5317\u4
|
|
|
14114
14302
|
method: opts.method,
|
|
14115
14303
|
outcome: opts.outcome,
|
|
14116
14304
|
runId: opts.runId,
|
|
14117
|
-
sinceTs: opts.sinceTs
|
|
14305
|
+
sinceTs: opts.sinceTs,
|
|
14306
|
+
resourceKey: opts.resourceKey
|
|
14118
14307
|
});
|
|
14119
14308
|
}
|
|
14120
14309
|
);
|
|
@@ -14128,11 +14317,18 @@ auditCmd.command("show").description("\u6309 auditId \u5C55\u793A\u5355\u6761\u5
|
|
|
14128
14317
|
auditCmd.command("restore-plan").description("\u6839\u636E\u5BA1\u8BA1 + \u5199\u524D\u5FEB\u7167\u751F\u6210\u8865\u507F\u5199\u8BA1\u5212\uFF08\u53EA\u8BFB\uFF0C\u4E0D\u8C03\u7528\u4E1A\u52A1\u63A5\u53E3\uFF09").requiredOption("--id <auditId>", "\u76EE\u6807\u5BA1\u8BA1 auditId").option("--json", "\u8F93\u51FA JSON", false).option("--json-out <path>", "\u843D\u76D8\uFF08\u4E0E --json \u4E92\u65A5\uFF09", void 0).action(async (opts) => {
|
|
14129
14318
|
await runAuditRestorePlan({ id: opts.id, json: Boolean(opts.json), jsonOut: opts.jsonOut });
|
|
14130
14319
|
});
|
|
14131
|
-
auditCmd.command("restore-apply").description(
|
|
14320
|
+
auditCmd.command("restore-apply").description(
|
|
14321
|
+
"\u6267\u884C\u8865\u507F\u5199\uFF08\u5C06\u8D44\u6E90\u5199\u56DE\u5FEB\u7167\u4E2D\u7684\u72B6\u6001\uFF09\uFF1B\u987B\u5148 restore-plan\uFF0C\u5E76\u7ECF --i-confirm + --ack-subsequent-writes \u5B88\u536B"
|
|
14322
|
+
).requiredOption("--id <auditId>", "\u76EE\u6807\u5BA1\u8BA1 auditId").option("--i-confirm", "\u786E\u8BA4\u5DF2\u9605\u8BFB restore-plan \u5E76\u540C\u610F\u6267\u884C", false).option(
|
|
14323
|
+
"--ack-subsequent-writes <n>",
|
|
14324
|
+
"\u663E\u5F0F\u786E\u8BA4\u4F1A\u88AB\u672C\u6B21\u56DE\u9000\u8986\u76D6\u7684\u540C resourceKey \u540E\u7EED\u6210\u529F\u5199\u6570\u91CF\uFF1B\u5FC5\u987B\u7B49\u4E8E restore-plan.guardChecks.ackSubsequentWrites",
|
|
14325
|
+
(v) => Number.parseInt(v, 10)
|
|
14326
|
+
).option("-t, --token <token>", "JWT\uFF08\u53EF\u9009\uFF0C\u4E0E\u5168\u5C40\u4E00\u81F4\uFF09", void 0).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF", false).option("--json", "stdout \u4EC5\u8F93\u51FA JSON", false).action(
|
|
14132
14327
|
async (opts) => {
|
|
14133
14328
|
await runAuditRestoreApply({
|
|
14134
14329
|
id: opts.id,
|
|
14135
14330
|
iConfirm: Boolean(opts.iConfirm),
|
|
14331
|
+
ackSubsequentWrites: opts.ackSubsequentWrites,
|
|
14136
14332
|
token: opts.token,
|
|
14137
14333
|
verbose: Boolean(opts.verbose),
|
|
14138
14334
|
json: Boolean(opts.json)
|
package/dist/skill/SKILL.md
CHANGED
package/dist/skill/_meta.json
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
- **补偿写**:再次调用写接口,把资源字段写回「写前快照」中的状态;**不是**数据库或平台的事务回滚 API。
|
|
6
6
|
- **真值在磁盘**:检索与定位一律通过 `siluzan-tso audit list` / `audit show` 读 `~/.siluzan/write-audit/tso/*.jsonl`,**不要**依赖对话中的历史列表。
|
|
7
|
+
- **resourceKey**:写审计在变异请求时由 `host+pathname` 派生,**同一资源的多次写共享同一 key**——`restore-plan` 据此扫描"目标 audit 之后该资源还有哪些写",让你看见回退会顺带覆盖什么。
|
|
7
8
|
|
|
8
9
|
## 写操作必填 commit
|
|
9
10
|
|
|
@@ -11,10 +12,9 @@
|
|
|
11
12
|
|
|
12
13
|
- 命令行:`--commit "说明"`(可出现在任意位置,与 `grep` 解析一致)
|
|
13
14
|
|
|
14
|
-
|
|
15
15
|
二者同时存在时 **命令行优先**。缺省则 CLI **stderr 报错并退出**,不发起 HTTP。
|
|
16
16
|
|
|
17
|
-
`audit list`、`audit show`、`audit restore-plan` 不产生业务写,不要求 commit。`audit restore-apply` 须加 `--i-confirm`;若仍未传 commit,会自动使用 `compensate:restore:<auditId>` 作为本条补偿写的说明。
|
|
17
|
+
`audit list`、`audit show`、`audit restore-plan` 不产生业务写,不要求 commit。`audit restore-apply` 须加 `--i-confirm` 与 `--ack-subsequent-writes`;若仍未传 commit,会自动使用 `compensate:restore:<auditId>` 作为本条补偿写的说明。
|
|
18
18
|
|
|
19
19
|
## 可选 runId
|
|
20
20
|
|
|
@@ -22,17 +22,80 @@
|
|
|
22
22
|
|
|
23
23
|
## 写前快照(白名单)
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
仅当 PUT/PATCH 命中以下 **Google 广告网关** 路径时,CLI 会在写前自动 `GET` 同 URL(去 query),快照存于 `write-audit/tso/snapshots/<auditId>.json`,审计行含 `preSnapshotRef`:
|
|
26
|
+
|
|
27
|
+
1. `/SemManagement/...` —— 历史在用的优化/系列侧接口
|
|
28
|
+
2. `/campaignmanagement/campaign/{accountId}/{campaignId}` —— 广告系列单资源详情(`ad campaign-status / campaign-delete / campaign-edit` 走这里)
|
|
29
|
+
|
|
30
|
+
未命中时审计行只有 `preSnapshotSkipped`、不会阻断主请求;`restore-plan` 也会返回 `supportability.code = "no_snapshot"` 并给出"人工反向写"的 hint。
|
|
31
|
+
|
|
32
|
+
明确**未纳入**白名单的资源(业务代码绕开了"单资源 GET",要么端点不可靠、要么 GET shape 与 PUT body 不对齐——盲目 apply 会写错形状):
|
|
33
|
+
|
|
34
|
+
- `/adgroupnmanagement/adgroup/{acc}/{id}`(业务走 list + filter)
|
|
35
|
+
- `/admanagement/campaign/{acc}/{adId}`(业务走 list + filter)
|
|
36
|
+
- `/keywordmanagement/Keyword/{acc}/batch`(批量 PUT,body 是数组)
|
|
37
|
+
- `/negativekeywordmanagement/negativekeyword/{acc}/{id}`(业务走 list + filter)
|
|
38
|
+
- `/campaignmanagement/` 下的 `v2/list/...`、`v2/(targeted|excluded)locations/...`、`criterion/...`、`geolocations/...`
|
|
39
|
+
|
|
40
|
+
POST 创建型与 DELETE 删除型 **整体**不在快照白名单内,`restore-plan` 会给出 `unsupported_method_post` / `unsupported_method_delete` 的明确诊断与下一步建议。
|
|
41
|
+
|
|
42
|
+
## restore-plan 输出结构(Agent 决策依据)
|
|
43
|
+
|
|
44
|
+
```jsonc
|
|
45
|
+
{
|
|
46
|
+
"auditDir": "...",
|
|
47
|
+
"plan": {
|
|
48
|
+
"auditId": "...",
|
|
49
|
+
"resourceKey": "googleapi.mysiluzan.com/SemManagement/...",
|
|
50
|
+
"supportability": {
|
|
51
|
+
"code": "ready_put_patch | audit_not_found | v1_record_no_audit_id | not_success_write | no_snapshot | snapshot_missing_or_invalid | unsupported_method_post | unsupported_method_delete | unsupported_method_other",
|
|
52
|
+
"reason": "一句话原因",
|
|
53
|
+
"hint": "仅 unsupported 时给出的人/Agent 下一步建议"
|
|
54
|
+
},
|
|
55
|
+
"willMutate": true,
|
|
56
|
+
"target": { "ts": "...", "method": "PUT", "pathname": "...", "outcome": "success", "commit": "...", "invokedCommand": "...", "httpStatus": 200 },
|
|
57
|
+
"snapshot": { "capturedAt": "...", "originalMethod": "PUT", "originalUrl": "...", "bodyUtf8Length": 1234, "bodySha256Prefix": "..." },
|
|
58
|
+
"subsequentWrites": [
|
|
59
|
+
{ "auditId": "...", "ts": "...", "method": "PUT", "outcome": "success", "pathname": "...", "commit": "...", "hasSnapshot": true }
|
|
60
|
+
],
|
|
61
|
+
"guardChecks": { "ackSubsequentWrites": 0 },
|
|
62
|
+
"steps": [{ "order": 1, "method": "PUT", "url": "...", "body": { /* snapshot.body */ }, "bodyUtf8Length": 1234, "bodySha256Prefix": "..." }]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
要点:
|
|
68
|
+
|
|
69
|
+
- `supportability.code` 是分流路标,Agent 必须据此判断是否进入 apply 分支。
|
|
70
|
+
- `subsequentWrites` 列出 **同 resourceKey、ts > target.ts** 的全部写(成功+失败)。`guardChecks.ackSubsequentWrites` 则是其中**成功**的数量——`restore-apply` 会要求显式确认。
|
|
71
|
+
- `steps` 仅在 `supportability.code === "ready_put_patch"` 时非空,单步 PUT/PATCH 重放 `snapshot.body`。
|
|
72
|
+
|
|
73
|
+
## restore-apply 守卫
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
siluzan-tso audit restore-apply \
|
|
77
|
+
--id <auditId> \
|
|
78
|
+
--i-confirm \
|
|
79
|
+
--ack-subsequent-writes <plan.guardChecks.ackSubsequentWrites>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- `--i-confirm`:人/Agent 已读 plan,承担覆盖风险。
|
|
83
|
+
- `--ack-subsequent-writes <N>`:必须等于 plan 显示的成功后续写数量。**apply 时会再 build 一次上下文**:如果 N 与最新扫描结果不一致(说明 plan→apply 期间又发生了新写),CLI 会拒绝执行——这是防 TOCTOU 的关键守卫。
|
|
84
|
+
|
|
85
|
+
## 自主回退工作流(Agent / 人类)
|
|
86
|
+
|
|
87
|
+
1. **定位 audit**:`siluzan-tso audit list --days 14 --match "关键词" --json`。
|
|
88
|
+
2. **看完整审计行**:`siluzan-tso audit show --id <auditId> --json`,确认 `preSnapshotFileReadable === true`。
|
|
89
|
+
3. **生成回退计划**:`siluzan-tso audit restore-plan --id <auditId> --json`。
|
|
90
|
+
- 检查 `plan.supportability.code === "ready_put_patch"`。
|
|
91
|
+
- **必读** `plan.subsequentWrites`:若存在成功的后续写,意味着回退会覆盖那些更晚的修改;按需可先用 `siluzan-tso audit list --resource-key "<plan.resourceKey>" --json` 把同资源全量历史拉出来交叉确认。
|
|
92
|
+
- 决定是接受这个覆盖,还是放弃回退/换另一种修复路径。
|
|
93
|
+
4. **执行**:`siluzan-tso audit restore-apply --id <auditId> --i-confirm --ack-subsequent-writes <N>`,N 来自 plan.guardChecks.ackSubsequentWrites。
|
|
94
|
+
|
|
95
|
+
**禁止**跳过 `restore-plan` 直接 `restore-apply`。
|
|
96
|
+
**禁止**根据 `commit` 文本编造请求体;**必须以** `restore-plan` 输出的 `steps[].body` 为准。
|
|
97
|
+
**禁止**在 `subsequentWrites` 非空且未与用户确认的情况下,盲目用 plan 中的数字 ack 后强行 apply。
|
|
35
98
|
|
|
36
99
|
## v1 历史审计
|
|
37
100
|
|
|
38
|
-
旧版 JSONL 无 `auditId`/`commit`/`preSnapshotRef
|
|
101
|
+
旧版 JSONL 无 `auditId`/`commit`/`preSnapshotRef`,`restore-plan` 会返回 `supportability.code === "v1_record_no_audit_id"` / `audit_not_found`,无法 `restore-apply`,仅可作 `audit show` 参考。
|