@hifullmoon/aicommit 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.aicommit.config.example.json +0 -10
- package/CHANGELOG.md +24 -1
- package/README.md +11 -36
- package/README.zh-CN.md +11 -36
- package/SECURITY.md +0 -1
- package/bin/aicommit.js +0 -18
- package/docs/distribution.md +1 -1
- package/docs/privacy.md +18 -20
- package/docs/provider-compatibility.md +13 -25
- package/docs/team-policy.md +2 -2
- package/docs/troubleshooting.md +16 -19
- package/package.json +1 -1
- package/src/api.js +3 -20
- package/src/cli.js +16 -130
- package/src/completion.js +1 -16
- package/src/config-command.js +24 -6
- package/src/config-paths.js +31 -0
- package/src/config.js +22 -42
- package/src/doctor.js +1 -12
- package/src/git.js +1 -1
- package/src/main.js +2 -26
- package/src/provider-presets.js +5 -95
- package/src/providers.js +5 -6
- package/src/setup.js +18 -4
- package/src/split.js +4 -119
- package/src/utils.js +1 -1
- package/docs/examples/extension/aicommit-extension.json +0 -9
- package/docs/examples/extension/index.mjs +0 -32
- package/docs/extensions.md +0 -93
- package/docs/provider-presets.md +0 -113
- package/schemas/aicommit-extension.schema.json +0 -27
- package/src/extension-runner.mjs +0 -36
- package/src/extensions.js +0 -426
- package/src/metrics.js +0 -375
- package/src/preset-command.js +0 -91
package/src/split.js
CHANGED
|
@@ -62,13 +62,7 @@ import {
|
|
|
62
62
|
splitCheckpointPath,
|
|
63
63
|
writeSplitCheckpoint,
|
|
64
64
|
} from './split-checkpoint.js';
|
|
65
|
-
import {
|
|
66
|
-
discoverSplitHunks,
|
|
67
|
-
fallbackHunkGroups,
|
|
68
|
-
stripHunkCatalog,
|
|
69
|
-
validateHunkTransaction,
|
|
70
|
-
} from './split-hunks.js';
|
|
71
|
-
import { extensionHostFor } from './extensions.js';
|
|
65
|
+
import { fallbackHunkGroups, stripHunkCatalog, validateHunkTransaction } from './split-hunks.js';
|
|
72
66
|
import { runModelTask } from './generation-ui.js';
|
|
73
67
|
|
|
74
68
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -1321,23 +1315,6 @@ export function executeSplit(
|
|
|
1321
1315
|
|
|
1322
1316
|
// Returns a structured result when split mode handled the run; false means
|
|
1323
1317
|
// "fall back to the normal single-commit flow" for a lone interactive file.
|
|
1324
|
-
export async function validateSplitExtensionMessages(groups, config, warnings = []) {
|
|
1325
|
-
const host = extensionHostFor(config);
|
|
1326
|
-
if (!host) return [];
|
|
1327
|
-
const policy = normalizeCommitPolicy(config.commitPolicy, config.language);
|
|
1328
|
-
const violations = [];
|
|
1329
|
-
for (const [index, group] of groups.entries()) {
|
|
1330
|
-
const issues = await host.validateMessage(group.message, policy);
|
|
1331
|
-
const errors = issues.filter((item) => item.severity === 'error');
|
|
1332
|
-
for (const issue of issues.filter((item) => item.severity === 'warning')) {
|
|
1333
|
-
const warning = `Split group ${index + 1}: ${issue.message}`;
|
|
1334
|
-
if (!warnings.includes(warning)) warnings.push(warning);
|
|
1335
|
-
}
|
|
1336
|
-
if (errors.length) violations.push({ group: index + 1, errors });
|
|
1337
|
-
}
|
|
1338
|
-
return violations;
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
1318
|
export async function splitFlow(
|
|
1342
1319
|
config,
|
|
1343
1320
|
projectRoot,
|
|
@@ -1348,7 +1325,6 @@ export async function splitFlow(
|
|
|
1348
1325
|
machineOutput = false,
|
|
1349
1326
|
provider = null,
|
|
1350
1327
|
exportPlanPath = null,
|
|
1351
|
-
splitHunks = false,
|
|
1352
1328
|
} = {},
|
|
1353
1329
|
) {
|
|
1354
1330
|
const reasoningEnabled = config.reasoning.mode === 'on';
|
|
@@ -1411,42 +1387,13 @@ export async function splitFlow(
|
|
|
1411
1387
|
throw fail(ERROR_CATEGORIES.GIT_STATE, 'No changes to commit.', { reported: true });
|
|
1412
1388
|
}
|
|
1413
1389
|
|
|
1414
|
-
if (allFiles.length === 1 && !yes && !exportPlanPath
|
|
1390
|
+
if (allFiles.length === 1 && !yes && !exportPlanPath) {
|
|
1415
1391
|
console.log('\n ' + chalk.dim('Only one changed file — falling back to single-commit mode.'));
|
|
1416
1392
|
return false;
|
|
1417
1393
|
}
|
|
1418
1394
|
|
|
1419
1395
|
const branch = getBranch(projectRoot);
|
|
1420
1396
|
const head = hasHead(projectRoot);
|
|
1421
|
-
const baseHead = head ? readGit(['rev-parse', 'HEAD'], projectRoot).trim() : null;
|
|
1422
|
-
let hunkSnapshots = null;
|
|
1423
|
-
let hunkMode = false;
|
|
1424
|
-
if (splitHunks) {
|
|
1425
|
-
try {
|
|
1426
|
-
hunkSnapshots = captureCheckpointSnapshots(projectRoot, scope, allFiles);
|
|
1427
|
-
const discovered = discoverSplitHunks(projectRoot, baseHead, allFiles, hunkSnapshots);
|
|
1428
|
-
if (discovered.some((change) => change.hunks?.length)) {
|
|
1429
|
-
allFiles = discovered;
|
|
1430
|
-
hunkMode = true;
|
|
1431
|
-
} else {
|
|
1432
|
-
warnings.push('Experimental hunk split found no eligible multi-hunk text modifications.');
|
|
1433
|
-
console.log(
|
|
1434
|
-
' ' +
|
|
1435
|
-
chalk.yellow(
|
|
1436
|
-
'⚠ Hunk split found no eligible multi-hunk text modifications; using file-level planning.',
|
|
1437
|
-
),
|
|
1438
|
-
);
|
|
1439
|
-
}
|
|
1440
|
-
} catch (err) {
|
|
1441
|
-
warnings.push(`Experimental hunk discovery fell back to file-level planning: ${err.message}`);
|
|
1442
|
-
console.log(
|
|
1443
|
-
' ' +
|
|
1444
|
-
chalk.yellow(
|
|
1445
|
-
`⚠ Hunk discovery could not prove a safe patch boundary; using file-level planning (${sanitizeTerminalText(err.message)}).`,
|
|
1446
|
-
),
|
|
1447
|
-
);
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
1397
|
|
|
1451
1398
|
console.log(
|
|
1452
1399
|
'\n ' +
|
|
@@ -1461,10 +1408,6 @@ export async function splitFlow(
|
|
|
1461
1408
|
}
|
|
1462
1409
|
|
|
1463
1410
|
const contextReport = collectRepositoryContext(projectRoot, allFiles, config.repositoryContext);
|
|
1464
|
-
const extensionContext = await extensionHostFor(config)?.collectContext({
|
|
1465
|
-
files: allFiles,
|
|
1466
|
-
branch,
|
|
1467
|
-
});
|
|
1468
1411
|
config = {
|
|
1469
1412
|
...config,
|
|
1470
1413
|
commitPolicy: applyCommitlintPolicy(
|
|
@@ -1472,12 +1415,9 @@ export async function splitFlow(
|
|
|
1472
1415
|
contextReport.constraints,
|
|
1473
1416
|
config.language,
|
|
1474
1417
|
),
|
|
1475
|
-
repositoryContextText:
|
|
1476
|
-
.filter(Boolean)
|
|
1477
|
-
.join('\n\n'),
|
|
1418
|
+
repositoryContextText: contextReport.text,
|
|
1478
1419
|
};
|
|
1479
1420
|
warnings.push(...contextReport.warnings);
|
|
1480
|
-
warnings.push(...(extensionContext?.warnings || []));
|
|
1481
1421
|
console.log(
|
|
1482
1422
|
' ' + chalk.dim(`Context: ${sanitizeTerminalText(repositoryContextSummary(contextReport))}`),
|
|
1483
1423
|
);
|
|
@@ -1631,51 +1571,13 @@ export async function splitFlow(
|
|
|
1631
1571
|
});
|
|
1632
1572
|
}
|
|
1633
1573
|
|
|
1634
|
-
const validateOrFallbackHunks = () => {
|
|
1635
|
-
if (!hunkMode) return;
|
|
1636
|
-
const provisional = createSplitPlanArtifact({
|
|
1637
|
-
scope,
|
|
1638
|
-
baseHead,
|
|
1639
|
-
fingerprint: plannedStateFingerprint,
|
|
1640
|
-
language: config.language,
|
|
1641
|
-
commitPolicy: config.commitPolicy,
|
|
1642
|
-
changes: allFiles,
|
|
1643
|
-
groups,
|
|
1644
|
-
hunkMode: true,
|
|
1645
|
-
});
|
|
1646
|
-
try {
|
|
1647
|
-
validateHunkTransaction(projectRoot, provisional, hunkSnapshots);
|
|
1648
|
-
} catch (err) {
|
|
1649
|
-
warnings.push(`Experimental hunk plan fell back to file-level grouping: ${err.message}`);
|
|
1650
|
-
console.log(
|
|
1651
|
-
'\n ' +
|
|
1652
|
-
chalk.yellow(
|
|
1653
|
-
`⚠ Hunk plan was not lossless; falling back to file-level grouping (${sanitizeTerminalText(err.message)}).`,
|
|
1654
|
-
),
|
|
1655
|
-
);
|
|
1656
|
-
groups = fallbackHunkGroups(groups, allFiles);
|
|
1657
|
-
allFiles = stripHunkCatalog(allFiles);
|
|
1658
|
-
hunkMode = false;
|
|
1659
|
-
}
|
|
1660
|
-
};
|
|
1661
|
-
validateOrFallbackHunks();
|
|
1662
|
-
|
|
1663
1574
|
// Review / edit / regenerate loop
|
|
1664
1575
|
let regenCounts = groups.map(() => 0);
|
|
1665
1576
|
let planEdited = false;
|
|
1666
1577
|
let rewriteCount = 0;
|
|
1667
1578
|
|
|
1668
1579
|
while (true) {
|
|
1669
|
-
const extensionViolations = await validateSplitExtensionMessages(groups, config, warnings);
|
|
1670
1580
|
displayPlan(groups, allFiles);
|
|
1671
|
-
if (extensionViolations.length) {
|
|
1672
|
-
console.log('\n ' + chalk.yellow.bold('⚠ Extension validation blocked this plan:'));
|
|
1673
|
-
for (const violation of extensionViolations) {
|
|
1674
|
-
for (const issue of violation.errors) {
|
|
1675
|
-
console.log(` Group ${violation.group}: ${sanitizeTerminalText(issue.message)}`);
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
1581
|
|
|
1680
1582
|
const action = yes
|
|
1681
1583
|
? dryRun
|
|
@@ -1731,7 +1633,6 @@ export async function splitFlow(
|
|
|
1731
1633
|
const edited = await editPlan(groups, allFiles, config.language, config.commitPolicy);
|
|
1732
1634
|
if (edited) {
|
|
1733
1635
|
groups = edited;
|
|
1734
|
-
validateOrFallbackHunks();
|
|
1735
1636
|
regenCounts = groups.map(() => 0);
|
|
1736
1637
|
planEdited = true;
|
|
1737
1638
|
}
|
|
@@ -1814,22 +1715,6 @@ export async function splitFlow(
|
|
|
1814
1715
|
continue; // show the updated plan again
|
|
1815
1716
|
}
|
|
1816
1717
|
|
|
1817
|
-
if (extensionViolations.length) {
|
|
1818
|
-
const details = extensionViolations
|
|
1819
|
-
.map(
|
|
1820
|
-
(violation) =>
|
|
1821
|
-
`Split group ${violation.group}: ${violation.errors
|
|
1822
|
-
.map((item) => item.message)
|
|
1823
|
-
.join(' ')}`,
|
|
1824
|
-
)
|
|
1825
|
-
.join(' ');
|
|
1826
|
-
if (!yes) {
|
|
1827
|
-
console.log(chalk.dim('\n Edit the plan or regenerate its messages before continuing.\n'));
|
|
1828
|
-
continue;
|
|
1829
|
-
}
|
|
1830
|
-
throw fail(ERROR_CATEGORIES.RESPONSE_FORMAT, details);
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
1718
|
break; // commit, or finish the dry run
|
|
1834
1719
|
}
|
|
1835
1720
|
|
|
@@ -1841,7 +1726,7 @@ export async function splitFlow(
|
|
|
1841
1726
|
commitPolicy: config.commitPolicy,
|
|
1842
1727
|
changes: allFiles,
|
|
1843
1728
|
groups,
|
|
1844
|
-
hunkMode,
|
|
1729
|
+
hunkMode: false,
|
|
1845
1730
|
});
|
|
1846
1731
|
let writtenPlanPath = null;
|
|
1847
1732
|
if (exportPlanPath) {
|
package/src/utils.js
CHANGED
|
@@ -140,7 +140,7 @@ const SENSITIVE_URL_PARAMETER_RE =
|
|
|
140
140
|
// Provider endpoints occasionally need non-secret query parameters such as
|
|
141
141
|
// `api-version`, but credentials embedded in userinfo or well-known secret
|
|
142
142
|
// parameters must never be echoed to terminals, JSON inspection output, or
|
|
143
|
-
//
|
|
143
|
+
// diagnostics. Fragments are omitted because HTTP never sends them.
|
|
144
144
|
export function redactSensitiveUrl(value) {
|
|
145
145
|
try {
|
|
146
146
|
const url = new URL(String(value));
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export function contextProvider({ files }) {
|
|
2
|
-
const packages = [...new Set(files.map(({ path }) => path.split('/')[0]).filter(Boolean))];
|
|
3
|
-
return { text: `Changed top-level areas: ${packages.join(', ')}`, warnings: [] };
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export function messageValidator({ message }) {
|
|
7
|
-
return {
|
|
8
|
-
issues: /(?:ACME|ticket)-\d+/i.test(message)
|
|
9
|
-
? []
|
|
10
|
-
: [{ severity: 'warning', code: 'ticket', message: 'consider including a ticket id' }],
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function providerAdapter({ operation, config, request, response, reasoning }) {
|
|
15
|
-
if (operation === 'buildRequest') {
|
|
16
|
-
return {
|
|
17
|
-
model: config.modelId,
|
|
18
|
-
input: request.messages,
|
|
19
|
-
max_output_tokens: request.maxTokens,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
if (operation === 'normalizeResponse') {
|
|
23
|
-
return {
|
|
24
|
-
content: response.output_text || '',
|
|
25
|
-
model: response.model,
|
|
26
|
-
usage: response.usage,
|
|
27
|
-
finishReason: response.status,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
if (operation === 'reasoningForFollowUp') return { ...reasoning, mode: 'off' };
|
|
31
|
-
throw new Error(`Unsupported providerAdapter operation: ${operation}`);
|
|
32
|
-
}
|
package/docs/extensions.md
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
# Extensions / 扩展
|
|
2
|
-
|
|
3
|
-
AICommit extension API v1 exposes three deliberately small interfaces without loading third-party code into the CLI process. Extensions are user-installed single-file ESM modules and are never enabled by repository config.
|
|
4
|
-
|
|
5
|
-
AICommit 扩展 API v1 提供三个刻意保持精简的接口,第三方代码不会加载到 CLI 主进程。扩展由用户以单文件 ESM 模块安装,仓库配置无权启用扩展。
|
|
6
|
-
|
|
7
|
-
## Security model / 安全模型
|
|
8
|
-
|
|
9
|
-
- Every manifest must declare `"permissions": { "credentials": false }`; v1 rejects every other value.
|
|
10
|
-
- The extension process receives no AICommit API key, credential-helper result, `HOME`/`USERPROFILE` value, or inherited secret environment variable.
|
|
11
|
-
- On Node.js 20+, the child runs with Node's permission model and may read only the packaged runner and its own `.mjs` entry. File writes, child processes, workers, and unrelated reads are denied.
|
|
12
|
-
- Node.js 18 remains supported for core AICommit. Executable extensions fail clearly instead of running without isolation; use Node.js 20+ when extensions are enabled.
|
|
13
|
-
- A context provider receives bounded branch/file metadata, a validator receives the candidate and normalized policy, and an adapter receives non-secret provider settings plus the request/response value needed for its operation.
|
|
14
|
-
- Extensions can observe the data passed to their selected capability and may have network access, including access to services reachable from the machine. Node's permission model is defense in depth, not a substitute for reviewing third-party code. Keep the manifest and entry in a dedicated directory; imports and multi-file packages are intentionally outside v1.
|
|
15
|
-
|
|
16
|
-
- 每个清单必须声明 `"permissions": { "credentials": false }`,v1 会拒绝其他值。
|
|
17
|
-
- 扩展进程不会收到 AICommit API key、credential helper 结果、`HOME`/`USERPROFILE` 值或继承的秘密环境变量。
|
|
18
|
-
- 在 Node.js 20+ 上,子进程使用 Node 权限模型,只能读取随包发布的 runner 和自己的 `.mjs` 入口;文件写入、子进程、worker 与无关文件读取均被拒绝。
|
|
19
|
-
- Node.js 18 仍可运行 AICommit 核心;启用扩展时会明确失败,而不会在无隔离条件下降级执行。扩展场景请使用 Node.js 20+。
|
|
20
|
-
- context provider 只收到受限的分支/文件元数据,validator 收到候选消息和标准化 policy,adapter 只收到非敏感 provider 设置及当前操作需要的请求/响应值。
|
|
21
|
-
- 扩展能看到其接口明确收到的数据,也可能访问网络,包括本机可达的服务。Node 权限模型是纵深防御,不能替代第三方代码审查。请把清单和入口放进独立目录;v1 有意不支持 import 和多文件扩展包。
|
|
22
|
-
|
|
23
|
-
## Manifest and configuration / 清单与配置
|
|
24
|
-
|
|
25
|
-
Copy the executable example in [`docs/examples/extension`](examples/extension), then add its absolute manifest path to the user config:
|
|
26
|
-
|
|
27
|
-
复制 [`docs/examples/extension`](examples/extension) 中的可执行示例,再把清单绝对路径写入用户配置:
|
|
28
|
-
|
|
29
|
-
```json
|
|
30
|
-
{
|
|
31
|
-
"extensions": {
|
|
32
|
-
"manifests": ["/Users/me/.aicommit/extensions/team-rules/aicommit-extension.json"],
|
|
33
|
-
"timeoutMs": 3000,
|
|
34
|
-
"maxContextChars": 2000
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
```json
|
|
40
|
-
{
|
|
41
|
-
"kind": "aicommit-extension",
|
|
42
|
-
"apiVersion": 1,
|
|
43
|
-
"id": "team-rules",
|
|
44
|
-
"version": "1.0.0",
|
|
45
|
-
"entry": "./index.mjs",
|
|
46
|
-
"capabilities": ["contextProvider", "messageValidator", "providerAdapter"],
|
|
47
|
-
"permissions": { "credentials": false }
|
|
48
|
-
}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Validate the config shape without resolving credentials, then exercise the installed code through a dry run or doctor:
|
|
52
|
-
|
|
53
|
-
先在不解析凭据的情况下校验配置结构,再通过 dry run 或 doctor 实际加载扩展:
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
aicommit config validate
|
|
57
|
-
aicommit --dry-run
|
|
58
|
-
aicommit doctor
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
The published JSON Schema is [`schemas/aicommit-extension.schema.json`](../schemas/aicommit-extension.schema.json).
|
|
62
|
-
|
|
63
|
-
## Interface contract / 接口契约
|
|
64
|
-
|
|
65
|
-
All exports may be synchronous or asynchronous and must return JSON-serializable values.
|
|
66
|
-
|
|
67
|
-
所有导出函数均可同步或异步执行,返回值必须可 JSON 序列化。
|
|
68
|
-
|
|
69
|
-
```js
|
|
70
|
-
export function contextProvider({ repository, branch, files }) {
|
|
71
|
-
return { text: 'bounded context text', warnings: [] };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function messageValidator({ message, policy }) {
|
|
75
|
-
return {
|
|
76
|
-
issues: [{ severity: 'error', code: 'ticket', message: 'ticket id required' }],
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function providerAdapter({ operation, config, request, response, reasoning }) {
|
|
81
|
-
if (operation === 'buildRequest') return { model: config.modelId, messages: request.messages };
|
|
82
|
-
if (operation === 'normalizeResponse') return { content: response.answer };
|
|
83
|
-
if (operation === 'reasoningForFollowUp') return { ...reasoning, mode: 'off' };
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
To select the adapter, set `"providerType": "extension:team-rules"`. Core code still owns endpoint validation, timeout/retry, HTTP transport, and Bearer authorization. Adapter-produced credential-like request fields are rejected. Therefore a new body dialect can be added without modifying the core Git or interaction flow, while custom credential schemes remain intentionally unsupported by extension API v1.
|
|
88
|
-
|
|
89
|
-
选择 adapter 时设置 `"providerType": "extension:team-rules"`。endpoint 校验、超时/重试、HTTP 传输和 Bearer 鉴权仍由核心负责。adapter 返回的疑似凭据字段会被拒绝。因此,新的请求/响应 body 方言无需修改核心 Git 或交互流程即可加入,而自定义鉴权方案在扩展 API v1 中暂不支持。
|
|
90
|
-
|
|
91
|
-
Validator errors participate in the same one-shot correction flow as built-in policy errors and fail closed if the extension crashes or returns malformed output. Context-provider failures become warnings so optional context cannot block a commit.
|
|
92
|
-
|
|
93
|
-
Validator 错误与内置 policy 错误共用一次纠正流程;扩展崩溃或返回格式错误时会 fail closed。Context provider 失败只产生 warning,避免可选上下文阻断提交。
|
package/docs/provider-presets.md
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
# Provider presets / Provider 预设
|
|
2
|
-
|
|
3
|
-
Provider presets are versioned setup data, not request code. The stable adapters in `src/providers.js` own request/response behavior; `presets/provider-presets.json` supplies a provider ID, display label, adapter ID, secure endpoint, a named model map, and the default model name. Each model supplies its API `modelId` and may include a label or bounded `extraBody` defaults. Adding another OpenAI-compatible service with the `custom` adapter does not change Git, interaction, or request orchestration code.
|
|
4
|
-
|
|
5
|
-
Provider preset 是带版本的 setup 数据,不是请求代码。`src/providers.js` 中的稳定 adapter 负责请求/响应行为;`presets/provider-presets.json` 提供 provider ID、显示名称、adapter ID、安全 endpoint、命名模型表和默认模型名。每个模型提供 API `modelId`,也可提供显示名称或有界的 `extraBody` 默认值。使用 `custom` adapter 新增另一个 OpenAI-compatible 服务时,不需要修改 Git、交互或请求编排代码。
|
|
6
|
-
|
|
7
|
-
## Compatibility contract / 兼容契约
|
|
8
|
-
|
|
9
|
-
Every manifest declares:
|
|
10
|
-
|
|
11
|
-
每份清单都声明:
|
|
12
|
-
|
|
13
|
-
```json
|
|
14
|
-
{
|
|
15
|
-
"kind": "aicommit-provider-presets",
|
|
16
|
-
"schemaVersion": 2,
|
|
17
|
-
"version": "2.0.0",
|
|
18
|
-
"compatibility": {
|
|
19
|
-
"coreMinimum": "1.5.1",
|
|
20
|
-
"coreMaximumExclusive": "3.0.0",
|
|
21
|
-
"adapterContract": 1
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
- `version` versions the independently replaceable preset data.
|
|
27
|
-
- The core range is inclusive at `coreMinimum` and exclusive at `coreMaximumExclusive`.
|
|
28
|
-
- Core prerelease versions and build metadata follow SemVer; build metadata is ignored for precedence.
|
|
29
|
-
- `adapterContract` declares the request-adapter interface expected by every entry.
|
|
30
|
-
- Every provider declares a non-empty `models` map and a `defaultModel` that references one entry. Model names are setup aliases; `modelId` is sent to the API.
|
|
31
|
-
- The runtime rejects unknown fields, duplicate/reserved IDs, unsupported adapters, remote HTTP, credentials, model-level `model`/`messages` overrides, oversized data, incompatible versions, and symlinked files.
|
|
32
|
-
|
|
33
|
-
- `version` 标识可独立替换的 preset 数据版本。
|
|
34
|
-
- core 范围包含 `coreMinimum`,不包含 `coreMaximumExclusive`。
|
|
35
|
-
- Core 的预发布版本与构建元数据遵循 SemVer;构建元数据不参与优先级比较。
|
|
36
|
-
- `adapterContract` 声明每个条目所依赖的请求 adapter 接口。
|
|
37
|
-
- 每个 Provider 都声明非空 `models`,并通过 `defaultModel` 引用其中一项。模型名是 setup 使用的别名,`modelId` 才会发送给 API。
|
|
38
|
-
- 运行时拒绝未知字段、重复/保留 ID、不支持的 adapter、远程 HTTP、凭据、模型级 `model`/`messages` 覆盖、超限数据、不兼容版本和符号链接文件。
|
|
39
|
-
|
|
40
|
-
The published JSON Schema is [`schemas/aicommit-provider-presets.schema.json`](../schemas/aicommit-provider-presets.schema.json). Runtime validation is authoritative and adds semantic/security checks that JSON Schema alone cannot express.
|
|
41
|
-
|
|
42
|
-
发布的 JSON Schema 位于 [`schemas/aicommit-provider-presets.schema.json`](../schemas/aicommit-provider-presets.schema.json)。运行时校验是最终依据,并补充 JSON Schema 无法完整表达的语义与安全检查。
|
|
43
|
-
|
|
44
|
-
## Inspect and validate / 检视与校验
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
aicommit preset path
|
|
48
|
-
aicommit preset show --output=json
|
|
49
|
-
aicommit preset validate --file=provider-presets.json --output=json
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
The bundled manifest is used unless `~/.aicommit/provider-presets.json` exists. `show` reports the selected source, preset version, compatibility declaration, and provider count. These commands do not resolve provider credentials.
|
|
53
|
-
|
|
54
|
-
默认使用随包发布的清单;如果存在 `~/.aicommit/provider-presets.json`,则优先使用用户清单。`show` 会报告选中来源、preset 版本、兼容声明和 provider 数量。这些命令不会解析 provider 凭据。
|
|
55
|
-
|
|
56
|
-
## Independent update and rollback / 独立更新与回滚
|
|
57
|
-
|
|
58
|
-
Validate before installation, install atomically, then verify setup sees the expected source/version:
|
|
59
|
-
|
|
60
|
-
安装前先校验,原子安装后再确认 setup 使用了预期来源与版本:
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
aicommit preset validate --file=provider-presets.json
|
|
64
|
-
aicommit preset install --file=provider-presets.json
|
|
65
|
-
aicommit preset show --output=json
|
|
66
|
-
aicommit setup
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
On a later install, AICommit writes the current valid manifest to `~/.aicommit/provider-presets.previous.json` before replacing it. Roll back without changing the core package:
|
|
70
|
-
|
|
71
|
-
后续安装时,AICommit 会先把当前有效清单写入 `~/.aicommit/provider-presets.previous.json`,再执行替换。无需更改 core 包即可回滚:
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
aicommit preset rollback
|
|
75
|
-
aicommit preset validate
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
If the active user manifest is malformed or incompatible, installation preserves its raw bytes as `provider-presets.invalid-<timestamp>.json` before repairing the active file. / 如果活动用户清单损坏或不兼容,安装会先将原始内容保存为 `provider-presets.invalid-<timestamp>.json`,再修复活动文件。
|
|
79
|
-
|
|
80
|
-
There is deliberately no automatic network updater. Obtain manifests through a trusted channel, review the endpoint/model changes, and validate locally before installation.
|
|
81
|
-
|
|
82
|
-
系统刻意不提供自动联网更新器。请通过可信渠道取得清单,审阅 endpoint/模型变更,并在安装前进行本地校验。
|
|
83
|
-
|
|
84
|
-
## Add a compatible provider / 新增兼容 provider
|
|
85
|
-
|
|
86
|
-
Add one entry to a copied manifest, bump `version`, validate, and install it:
|
|
87
|
-
|
|
88
|
-
在清单副本中增加一个条目、提升 `version`,然后校验并安装:
|
|
89
|
-
|
|
90
|
-
```json
|
|
91
|
-
{
|
|
92
|
-
"id": "acme",
|
|
93
|
-
"label": "Acme Compatible",
|
|
94
|
-
"adapter": "custom",
|
|
95
|
-
"apiUrl": "https://api.acme.example/v1/chat/completions",
|
|
96
|
-
"defaultModel": "fast",
|
|
97
|
-
"models": {
|
|
98
|
-
"fast": {
|
|
99
|
-
"label": "Acme Chat",
|
|
100
|
-
"modelId": "acme-chat"
|
|
101
|
-
},
|
|
102
|
-
"quality": {
|
|
103
|
-
"label": "Acme Reasoner",
|
|
104
|
-
"modelId": "acme-reasoner",
|
|
105
|
-
"extraBody": { "reasoning": true }
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
If the service speaks an existing adapter contract, no core flow changes are required. A genuinely new wire protocol belongs in a provider adapter extension, not in preset data.
|
|
112
|
-
|
|
113
|
-
如果服务符合现有 adapter 契约,就不需要修改 core 流程。真正的新 wire protocol 应实现 provider adapter 扩展,而不是塞入 preset 数据。
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://github.com/hi-fullmoon/AICommit/schemas/aicommit-extension.schema.json",
|
|
4
|
-
"title": "AICommit extension manifest v1",
|
|
5
|
-
"type": "object",
|
|
6
|
-
"additionalProperties": false,
|
|
7
|
-
"required": ["kind", "apiVersion", "id", "version", "entry", "capabilities", "permissions"],
|
|
8
|
-
"properties": {
|
|
9
|
-
"kind": { "const": "aicommit-extension" },
|
|
10
|
-
"apiVersion": { "const": 1 },
|
|
11
|
-
"id": { "type": "string", "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$" },
|
|
12
|
-
"version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$" },
|
|
13
|
-
"entry": { "type": "string", "pattern": "^[^/].*\\.mjs$" },
|
|
14
|
-
"capabilities": {
|
|
15
|
-
"type": "array",
|
|
16
|
-
"minItems": 1,
|
|
17
|
-
"uniqueItems": true,
|
|
18
|
-
"items": { "enum": ["contextProvider", "messageValidator", "providerAdapter"] }
|
|
19
|
-
},
|
|
20
|
-
"permissions": {
|
|
21
|
-
"type": "object",
|
|
22
|
-
"additionalProperties": false,
|
|
23
|
-
"required": ["credentials"],
|
|
24
|
-
"properties": { "credentials": { "const": false } }
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
package/src/extension-runner.mjs
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
|
|
3
|
-
const MAX_INPUT_CHARS = 256_000;
|
|
4
|
-
|
|
5
|
-
for (const method of ['log', 'info', 'warn', 'error']) {
|
|
6
|
-
console[method] = (...values) => {
|
|
7
|
-
process.stderr.write(`${values.map((value) => String(value)).join(' ')}\n`);
|
|
8
|
-
};
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function write(value) {
|
|
12
|
-
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
let input = '';
|
|
16
|
-
process.stdin.setEncoding('utf8');
|
|
17
|
-
for await (const chunk of process.stdin) {
|
|
18
|
-
input += chunk;
|
|
19
|
-
if (input.length > MAX_INPUT_CHARS) throw new Error('Extension input exceeds 256000 characters.');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const request = JSON.parse(input);
|
|
24
|
-
const source = await readFile(request.entry, 'utf8');
|
|
25
|
-
const moduleUrl = `data:text/javascript;base64,${Buffer.from(source).toString('base64')}`;
|
|
26
|
-
const extension = await import(moduleUrl);
|
|
27
|
-
const handler = extension[request.capability];
|
|
28
|
-
if (typeof handler !== 'function') {
|
|
29
|
-
throw new Error(`Extension does not export ${request.capability}().`);
|
|
30
|
-
}
|
|
31
|
-
const result = await handler(request.input);
|
|
32
|
-
write({ ok: true, result });
|
|
33
|
-
} catch (error) {
|
|
34
|
-
write({ ok: false, error: String(error?.message || error).slice(0, 2000) });
|
|
35
|
-
process.exitCode = 1;
|
|
36
|
-
}
|