修:draw-ui / oil-motion 原被当子模块指针收录 ⇒ 改为正常文件入库(两份内容原先对别人是空的)

一、问题(本轮实测)
`draw-ui` 与 `oil-motion` 目录里**各自带一个内嵌 `.git`** ⇒ 上一次提交把它们记成了 **gitlink(子模块指针)**
⇒ 仓库里只存了一个不属于任何远端的 commit id,**别人克隆下来这两份是空的** ✗(`git status` 显示 ` m draw-ui` / ` m oil-motion` = 子模块内容有改动)。

二、处置(可回退)
· 把两处的 `.git` **挪走**(⛔ 不是删除)⇒ `归档/内嵌git-20261008/{draw-ui,oil-motion}.git`;
· `git rm --cached` 掉那两个 gitlink,再 `git add` 两个目录 ⇒ **按正常文件入库**(内容才真的进仓库)。

三、副作用(如实记)
挪走 `.git` 后,这两个技能**不能再原地 `git pull` 取上游更新**(要更新得重新拉一份覆盖);
如需恢复其本地仓库,把 `归档/内嵌git-20261008/` 里的 `.git` 挪回原处即可。
This commit is contained in:
admin committed 2026-10-08 22:29:52 +08:00
1 parent e03465c398
commit 237a09a5b0
161 files changed
+19429 -2

No files matched your search

@@ -0,0 +1,145 @@
"use strict";
const get = (id) => document.getElementById(id);
const form = get('credential-form');
const button = get('save');
let inputs = [];
let meta;
let busy = false;
const saveLabel = () => inputs.some((input, index) => input.value.trim() && meta.fields[index].configured)
? '替换并保存' : (meta.page.saveLabel ?? (meta.fields.length === 1 ? meta.fields[0].ui?.saveLabel : undefined) ?? '保存');
async function request(url, options = {}) {
let response;
try {
response = await fetch(url, { ...options, credentials: 'same-origin', cache: 'no-store',
headers: { 'Content-Type': 'application/json', 'X-Local-Request': '1', ...options.headers }, signal: AbortSignal.timeout(25_000) });
}
catch {
throw new Error('未收到服务回复,请刷新确认保存结果,不要重复提交。');
}
const result = await response.json();
if (!response.ok)
throw new Error(result.error ?? '操作未完成,请重试。');
return result;
}
function message(text) { const node = get('message'); node.textContent = text; node.hidden = false; }
function clearInputs() { inputs.forEach(input => { input.value = ''; }); }
function done() {
clearInputs();
form.hidden = true;
get('heading').textContent = '已保存';
get('hint').hidden = true;
message('可以关闭此页,回到对话继续。');
}
function update() {
button.disabled = busy || !meta || !['waiting', 'partial'].includes(meta.outcome)
|| !inputs.some(input => input.value.trim())
|| inputs.some((input, index) => !meta.fields[index].configured && !input.value.trim());
if (meta && !busy)
button.textContent = saveLabel();
}
function render() {
clearInputs();
inputs = [];
const multi = meta.fields.length > 1;
form.classList.toggle('multi', multi);
const container = get('fields');
container.replaceChildren();
meta.fields.forEach((field, index) => {
const wrapper = document.createElement('div');
wrapper.className = 'field';
const label = document.createElement('label');
label.htmlFor = 'secret-' + index;
label.textContent = field.label;
if (!multi)
label.className = 'visually-hidden';
if (field.configured) {
const state = document.createElement('span');
state.className = 'field-state';
state.textContent = '已配置';
label.append(state);
}
const input = document.createElement('input');
input.id = label.htmlFor;
input.type = 'password';
input.autocomplete = 'new-password';
input.autocapitalize = 'off';
input.spellcheck = false;
input.maxLength = 2500;
input.required = !field.configured;
input.setAttribute('aria-describedby', 'hint message');
input.placeholder = field.configured ? '留空保留,输入则替换' : (field.ui?.placeholder ?? '粘贴 API Key');
input.disabled = !['waiting', 'partial'].includes(meta.outcome);
wrapper.append(label, input);
container.append(wrapper);
inputs.push(input);
});
get('context').textContent = meta.page.label ?? (multi ? meta.fields.length + ' 项凭据' : meta.fields[0].label);
const title = meta.page.title ?? (multi ? '输入密钥' : meta.fields[0].ui?.title) ?? '输入密钥';
get('heading').textContent = title;
document.title = title;
const storage = [...new Set(meta.fields.map(field => field.storage))].join('、');
get('hint').textContent = '仅保存到' + storage + (meta.fields.some(field => field.configured) ? ' · 已配置项留空保留' : '');
update();
if (meta.outcome === 'saved')
done();
else if (meta.outcome === 'partial')
message('上次仅部分保存。请核对已配置项,补填未完成项后重试。');
else if (meta.outcome !== 'waiting')
message('本次配置已结束,请重新打开入口。');
}
form.addEventListener('input', () => { update(); get('message').hidden = true; });
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (busy || button.disabled)
return;
busy = true;
update();
inputs.forEach(input => { input.disabled = true; });
button.textContent = '正在保存…';
try {
const entries = inputs.flatMap((input, index) => input.value.trim() ? [{ credential: meta.fields[index].credential,
value: input.value, revision: meta.fields[index].revision, replaceExisting: meta.fields[index].configured }] : []);
const result = await request('/api/save', { method: 'POST', body: JSON.stringify({ entries }) });
if (result.status === 'saved') {
meta.outcome = 'saved';
done();
}
else {
clearInputs();
meta = await request('/api/meta');
render();
const statuses = { saved: '已保存', failed: '未确认成功', not_attempted: '未尝试' };
message((result.results ?? []).map(item => (meta.fields.find(field => field.credential === item.credential)?.label ?? '凭据') + ':' + (statuses[item.status] ?? '请核对')).join(';')
+ '。请核对状态后重新填写未完成项。');
}
}
catch (error) {
clearInputs();
try {
meta = await request('/api/meta');
render();
}
catch {
meta.outcome = 'unknown';
}
if (meta.outcome !== 'saved')
message(error instanceof Error ? error.message : '保存未完成,请刷新确认结果。');
}
finally {
clearInputs();
busy = false;
inputs.forEach(input => { input.disabled = !['waiting', 'partial'].includes(meta.outcome); });
update();
}
});
window.addEventListener('pagehide', clearInputs);
async function initialize() {
const token = location.hash.slice(1);
if (token) {
history.replaceState(null, '', location.pathname);
await request('/api/session', { method: 'POST', headers: { Authorization: 'Bearer ' + token }, body: '{}' });
}
meta = await request('/api/meta');
render();
}
initialize().catch(error => { get('context').textContent = '暂时无法连接'; message(error instanceof Error ? error.message : '请重新打开配置入口。'); });
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><rect width="40" height="40" rx="12" fill="#292929"/><path d="M12 19v-4a8 8 0 0 1 16 0v4" fill="none" stroke="#eeeeee" stroke-width="3"/><rect x="9" y="18" width="22" height="16" rx="5" fill="#eeeeee"/><circle cx="20" cy="25" r="2" fill="#292929"/></svg>

After

Width:  |  Height:  |  Size: 315 B

@@ -0,0 +1,28 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light" />
<meta name="referrer" content="no-referrer" />
<title>输入密钥</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="/style.css" />
<script type="module" src="/app.js"></script>
</head>
<body>
<main>
<h1 id="heading">输入密钥</h1>
<p id="context" class="context">正在连接…</p>
<form id="credential-form" autocomplete="off">
<div id="fields">
<label for="secret" class="visually-hidden">密钥</label>
<input id="secret" name="secret" type="password" placeholder="粘贴 API Key" autocomplete="new-password" autocapitalize="off" autocorrect="off" spellcheck="false" maxlength="2500" required disabled aria-describedby="hint message" />
</div>
<button id="save" type="submit" disabled>保存</button>
</form>
<p id="hint" class="hint">仅保存到本机系统凭据库</p>
<p id="message" class="message" role="status" aria-live="polite" hidden></p>
</main>
</body>
</html>
@@ -0,0 +1,143 @@
@charset "UTF-8";
:root {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
color: #282828;
background: #fafafa;
font-synthesis: none;
color-scheme: light;
-webkit-font-smoothing: antialiased;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100svh;
display: grid;
place-items: center;
padding: 24px;
}
main {
width: 100%;
max-width: 320px;
margin-top: -5vh;
}
h1 {
margin: 0 0 6px;
font-size: 20px;
font-weight: 580;
letter-spacing: -.45px;
line-height: 1.45;
}
.context {
margin: 0 0 19px;
font-size: 11.5px;
color: #777777;
line-height: 1.6;
overflow-wrap: anywhere;
}
form {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
gap: 8px;
}
#fields, .field { display: contents; }
form.multi { grid-template-columns: 1fr; gap: 18px; }
.multi #fields { display: grid; gap: 17px; }
.multi .field { display: grid; gap: 7px; }
.multi label { font-size: 12px; line-height: 1.5; overflow-wrap: anywhere; }
.field-state { color: #818181; font-size: 10.5px; margin-left: 7px; }
.multi button { justify-self: end; height: 36px; }
body:has(form.multi) { align-items: safe center; }
body:has(form.multi) main { margin-top: 0; }
input, button {
font: inherit;
border-radius: 8px;
height: 42px;
}
input {
width: 100%;
min-width: 0;
background: #fff;
border: 1px solid #dddddd;
padding: 0 12px;
font-size: 14px;
color: #303030;
outline: none;
box-shadow: 0 1px 2px #00000003;
transition: border-color .15s, box-shadow .15s;
}
input::placeholder {
color: #999999;
font-size: 12px;
}
input:hover:not(:disabled) { border-color: #c5c5c5; }
input:focus {
border-color: #929292;
box-shadow: 0 0 0 3px #eeeeee, 0 1px 2px #00000003;
}
input:disabled { background: #f4f4f4; }
button {
min-width: 62px;
padding: 0 15px;
background: #303030;
border: 1px solid #292929;
color: #fafafa;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
cursor: pointer;
box-shadow: inset 0 1px 0 #ffffff0d, 0 1px 2px #0000000c;
transition: background .15s, border-color .15s, transform .12s;
}
button:hover:not(:disabled) {
background: #454545;
border-color: #3c3c3c;
}
button:active:not(:disabled) { transform: translateY(1px); }
button:disabled {
background: #ececec;
border-color: #e3e3e3;
color: #949494;
box-shadow: none;
cursor: default;
}
button:focus-visible {
outline: 3px solid #c9c9c9;
outline-offset: 3px;
}
.hint {
margin: 12px 0 0;
font-size: 10.5px;
color: #818181;
line-height: 1.7;
}
.message {
margin: 12px 0 0;
font-size: 11px;
color: #5c5c5c;
line-height: 1.8;
}
.message.success { color: #5c5c5c; }
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
[hidden] { display: none !important; }
@media (max-width: 480px) {
main { margin-top: -8vh; }
input { font-size: 16px; }
input, button { height: 44px; }
}
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; }
}