fix: 修正凭据名称避免 DSH 重启失败
This commit is contained in:
1 parent
108e957c64
commit
7e2ec28b80
4 files changed
+57
-6
No files matched your search
@@ -141,6 +141,17 @@ git+https://git.crash.work/cneicy/dsh-imagegen.git
|
||||
- 在 DSH「插件」页面停用或卸载 `dsh-imagegen-local`。
|
||||
- 卸载不会自动删除已保存的图片和凭据。如需清理密钥,请先在生图设置里清除对应地址的密钥,再卸载。
|
||||
|
||||
### 1.0.1 凭据兼容修复(从 1.0.0 升级必读)
|
||||
|
||||
1.0.0 曾将凭据名称写成 `dsh-imagegen:<SHA-256>`,不符合 DSH 的 `scope/id` 校验规则,可能导致凭据服务在下次启动时失败。1.0.1 统一使用 `imagegen-local/endpoint-<SHA-256>`;`endpoint-` 保证 ID 始终以字母开头,scope 与注册的 Host 插件名称一致。读、写、删除都使用同一格式。
|
||||
|
||||
- **DSH 已能启动**:先更新插件并重启 DSH,再在「设置 → 生图」重新保存 API Key。旧版配置的 Base URL、模型列表和原图不受影响。
|
||||
- **DSH 因旧记录无法启动**:先退出 DSH,安全备份 `$DSH_HOME/.credentials.yaml`,只移出 `records` 下本插件的旧条目(`dsh-imagegen:<哈希>`,或曾手动改成的 `dsh-imagegen/<哈希>`),保留其他凭据和 YAML 结构;启动后更新插件,再重新保存 API Key。备份含密钥,请勿上传。
|
||||
- 不要仅替换冒号:十六进制哈希可能以数字开头,仍不符合 ID 规则。
|
||||
- 插件不会自动读取备份、恢复被移出的旧密钥,或直接改写全局凭据文件。凭据服务先于插件加载,启动被旧记录阻断时,插件自身无法执行迁移。
|
||||
|
||||
回归测试现在在每次凭据读写、删除及持久化重载时校验名称,并覆盖哈希首字符的全部 16 种情况。
|
||||
|
||||
## 常见问题
|
||||
|
||||
**出现 HTTP 401 / 403**:检查 API Key、Base URL 以及账号是否有该模型的权限。
|
||||
|
||||
+3
-1
@@ -12,7 +12,9 @@ const dataRoot = join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'imagegen
|
||||
const settingsPath = join(dataRoot, 'settings.json');
|
||||
const outputs = join(dataRoot, 'outputs');
|
||||
const initial = { baseUrl: 'https://api.openai.com/v1', models: ['gpt-image-2', 'gpt-image-1.5'], defaultModel: '' };
|
||||
const keyFor = base => `dsh-imagegen:${createHash('sha256').update(base).digest('hex')}`;
|
||||
// DSH CredentialKey requires <scope>/<id>, each matching [a-z][a-z0-9-]*.
|
||||
// Use the registered plugin name as scope and prefix the digest: hex may start with a digit.
|
||||
const keyFor = base => `${name}/endpoint-${createHash('sha256').update(base).digest('hex')}`;
|
||||
const json = (value, status = 200) => new Response(JSON.stringify(value), { status, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' } });
|
||||
const schema = { type: 'object', additionalProperties: true };
|
||||
const text = value => ({ type: 'text', text: JSON.stringify(value, null, 2) });
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dsh-imagegen-local",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "生图:自定义 Images API、显式模型选择及对话生图工具",
|
||||
"type": "module",
|
||||
"main": "./lib/index.mjs",
|
||||
|
||||
+42
-4
@@ -5,6 +5,12 @@ import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, dirname, basename } from 'node:path';
|
||||
|
||||
// Match DSH credentials credentialKey/parseCredentialKey admission rules.
|
||||
// Validate at every service boundary AND on reload, unlike the original permissive mock.
|
||||
function validateCredentialName(key) {
|
||||
assert.match(key, /^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/);
|
||||
}
|
||||
|
||||
const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jVv8AAAAASUVORK5CYII=', 'base64');
|
||||
|
||||
test('native host integration using only a local image API', async t => {
|
||||
@@ -47,9 +53,9 @@ test('native host integration using only a local image API', async t => {
|
||||
} } },
|
||||
tools: { register(tool) { tools.set(tool.name, tool); return () => tools.delete(tool.name); } },
|
||||
credentials: {
|
||||
async readRecord(key) { return records.get(key); },
|
||||
async modifyRecord(key, change) { const value = await change(records.get(key)); records.set(key, value); return value; },
|
||||
async deleteRecord(key) { records.delete(key); },
|
||||
async readRecord(key) { validateCredentialName(key); return records.get(key); },
|
||||
async modifyRecord(key, change) { validateCredentialName(key); const value = await change(records.get(key)); records.set(key, value); return value; },
|
||||
async deleteRecord(key) { validateCredentialName(key); records.delete(key); },
|
||||
},
|
||||
attachments: { async saveImage(image) {
|
||||
assert.deepEqual(Buffer.from(image.data), PNG);
|
||||
@@ -90,7 +96,7 @@ test('native host integration using only a local image API', async t => {
|
||||
assert.ok(!JSON.stringify(value).includes(key));
|
||||
assert.equal(records.size, 1);
|
||||
const [credentialName, credential] = [...records][0];
|
||||
assert.match(credentialName, /^dsh-imagegen:[0-9a-f]{64}$/);
|
||||
assert.match(credentialName, /^imagegen-local\/endpoint-[0-9a-f]{64}$/);
|
||||
assert.deepEqual(credential, { kind: 'api-key', key });
|
||||
const disk = await readFile(join(home, 'imagegen', 'settings.json'), 'utf8');
|
||||
assert.deepEqual(JSON.parse(disk), settings);
|
||||
@@ -100,6 +106,18 @@ test('native host integration using only a local image API', async t => {
|
||||
assert.ok(!JSON.stringify(status).includes(key));
|
||||
assert.ok(!(await (await call('/api/imagegen/settings')).text()).includes(key));
|
||||
});
|
||||
await t.test('saved credentials survive strict reload and host reinitialization', async () => {
|
||||
const persisted = JSON.stringify({ version: 1, records: Object.fromEntries(records) });
|
||||
records.clear();
|
||||
for (const [address, value] of Object.entries(JSON.parse(persisted).records)) {
|
||||
validateCredentialName(address);
|
||||
records.set(address, value);
|
||||
}
|
||||
listeners.get('dispose')();
|
||||
routes.clear(); tools.clear();
|
||||
await host.apply(ctx);
|
||||
assert.equal((await tools.get('imagegen_status').execute()).configured, true);
|
||||
});
|
||||
await t.test('lists models using the local authenticated API', async () => {
|
||||
const response = await call('/api/imagegen/models', 'POST');
|
||||
assert.equal(response.status, 200);
|
||||
@@ -157,6 +175,26 @@ test('native host integration using only a local image API', async t => {
|
||||
assert.deepEqual(await readFile(value.images[0].path), PNG);
|
||||
assert.ok(!JSON.stringify(value).includes(key));
|
||||
});
|
||||
await t.test('all digest prefixes are valid and clearing only removes the selected endpoint', async () => {
|
||||
const originalRecords = new Map(records);
|
||||
const prefixes = new Set();
|
||||
for (let i = 0; prefixes.size < 16 && i < 512; i++) {
|
||||
const endpoint = { ...settings, baseUrl: `https://example.com/v1/provider-${i}` };
|
||||
const response = await call('/api/imagegen/settings', 'POST', { ...endpoint, apiKey: 'test-only-temporary-key' });
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal((await response.json()).configured, true);
|
||||
const address = [...records.keys()].find(item => !originalRecords.has(item));
|
||||
validateCredentialName(address);
|
||||
prefixes.add(address.split('endpoint-')[1][0]);
|
||||
assert.equal((await (await call('/api/imagegen/settings', 'POST', endpoint)).json()).configured, true);
|
||||
const cleared = await call('/api/imagegen/settings', 'POST', { ...endpoint, clearKey: true });
|
||||
assert.equal(cleared.status, 200);
|
||||
assert.equal((await cleared.json()).configured, false);
|
||||
assert.deepEqual(records, originalRecords);
|
||||
}
|
||||
assert.equal(prefixes.size, 16, 'exercise every hexadecimal leading character, including 0–9');
|
||||
await call('/api/imagegen/settings', 'POST', settings);
|
||||
});
|
||||
await t.test('file traversal, absolute paths and missing files return 404', async () => {
|
||||
for (const filename of ['../settings.json', '..\\settings.json', join(home, 'imagegen', 'settings.json'), '/etc/passwd', '', '123-00000000-0000-0000-0000-000000000000.png']) {
|
||||
assert.equal((await call(`/api/imagegen/file?name=${encodeURIComponent(filename)}`)).status, 404);
|
||||
|
||||
Reference in new issue
Block a user