t193:daemon UI 換 Wails——CIS 是硬要求,fyne 做不到

leo 2026-08-04 看過 v0.16.0 畫面後:
「功能都有了,但美感非常糟糕⋯⋯跟 CIS 完全無關,每個功能都開一個小小的 popup 視窗,
 非常缺乏整體感,這要理解的是原始的技術選擇是否出錯?」
「我的要求是符合 CIS,在風格上跟 portal 一樣」
「CIS 已經提供規範,你做的連 Logo 都沒放上去,這跟美不美有關係嗎?
 要求放進 CIS 是硬要求,你做為檢查有嗎?沒有怎麼交貨?」

## 我的兩個錯
1. **動工前沒提醒做不到**——decisions-summary.md:314 的 D-daemon-UI
   是我 07-27 自己寫的調研(結論:fyne 全自繪、CSS 套不進去),寫完就沒再看。
   今天動工前該查它卻直接開寫,浪費 leo 的時間與 token。
2. **沒做 CIS 檢查就交貨**——連 logo 都沒放。

## 換 Wails(WebView 殼)
前端就是 HTML/CSS ⇒ 可以直接用 portal 那份色票與 lockup。
- style.css 的色票/字體/紙張紋理**逐字取自** portal 的 :root
  (matrix/arcrun:console-ui/public/portal/index.html),不自創任何顏色
- CIS lockup 官方 PNG,淺色 ink 版/深色 paper 版,切換規則同 portal
  (避開 08-01 作廢的自產 SVG——字腔缺失)
- 對話框改**內嵌覆蓋層**,不再每個功能開一個小 popup
- 原生資料夾選擇器(macOS powerbox 會自動授予該資料夾存取權,
  fyne 自繪 picker 拿不到;未來上 Mac App Store 是硬需求)
- 同步中的圓點會呼吸 ⇒ 看得出來在動(issue #17)
- 無帳號時是 onboarding 引導,不是空白面板

## 連線邏輯逐字沿用,不重寫
connect.go 的 normalizePortalURL/fetchConfigByLogin 取自 arcrun-tray/main.go,
含 t86 個資外洩事故的防線(不同實例=新增帳號,絕不覆蓋舊帳號的資料夾)。
今天已犯過一次「重寫別人修好的東西還修得更差」,不再犯第二次。

## 新增 check-cis.sh(交貨前必跑)
機械檢查六類:官方色票逐個到齊/**不准出現非 CIS 色**/logo 真的放進去/
不可用作廢 SVG/深色模式/字體與紙張紋理同 portal/app icon。
實跑 14 項全過。以後「沒跑過就不准交」。

⚠️ 未驗:實際畫面要 leo 開來看(我開不了視窗)。
This commit is contained in:
2026-08-04 22:19:41 +08:00
parent cc6534dd39
commit bd0cead00e
30 changed files with 2856 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

@@ -0,0 +1,93 @@
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

+163
View File
@@ -0,0 +1,163 @@
// Arcrun 桌面 App — 前端(t193
//
// 全部畫面都在**同一個視窗**裡:狀態、清單、設定都用內嵌覆蓋層,
// 不再像 fyne 版那樣「每個功能開一個小小的 popup」(leo 08-04 點破)。
import './style.css';
const go = window.go.main.App;
const $ = (id) => document.getElementById(id);
// 跟隨系統深淺色——CIS 兩套色票都定義好了,這裡只負責切 data-theme
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const applyTheme = () =>
document.documentElement.setAttribute('data-theme', mq.matches ? 'dark' : 'light');
mq.addEventListener('change', applyTheme);
applyTheme();
// ── 覆蓋層(取代 popup 視窗)──
function openSheet(html, wire) {
$('sheet').innerHTML = html;
$('overlay').classList.add('on');
if (wire) wire();
}
function closeSheet() { $('overlay').classList.remove('on'); }
$('overlay').addEventListener('click', (e) => { if (e.target.id === 'overlay') closeSheet(); });
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeSheet(); });
const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
// ── 主畫面渲染 ──
let state = null;
function render(s) {
state = s;
$('ver').textContent = s.version ? '版本 ' + s.version : '';
$('statusBig').textContent = s.statusBig;
$('statusBig').classList.toggle('syncing', !!s.syncing);
$('statusSub').textContent = s.statusSub;
const list = $('list');
if (!s.accounts || s.accounts.length === 0) {
// 空狀態=onboarding,不是空白面板
list.innerHTML = `
<div class="empty">
<div class="t">先連上你的知識庫</div>
<div class="d">
Arcrun 會看守你指定的資料夾,<br/>
自動把文件整理成 AI 查得到的知識卡。<br/>
還沒有知識庫?先到 install.arcrun.dev 裝一個(免費)。
</div>
<button class="primary" id="obConnect">新增知識庫帳號</button>
<button id="obInstall">我還沒有知識庫</button>
</div>`;
$('obConnect').onclick = showConnect;
$('obInstall').onclick = () => go.OpenURL('https://install.arcrun.dev/');
return;
}
list.innerHTML = s.accounts.map((a) => `
<div class="acct"><span class="name">${esc(a.name)}</span><span class="host">${esc(a.host)}</span></div>
${(a.folders || []).map((f) => `
<div class="folder">
<span class="path" title="${esc(f.path)}">${esc(f.path)}</span>
<span class="tag">自動同步中</span>
<button class="ghost" data-rm="${esc(f.path)}" data-acc="${f.accIdx}">移除</button>
</div>`).join('')
|| `<div class="folder"><span class="path dim">還沒有資料夾——按下面的「加入資料夾」</span></div>`}
`).join('');
list.querySelectorAll('[data-rm]').forEach((b) => {
b.onclick = () => confirmRemove(Number(b.dataset.acc), b.dataset.rm);
});
}
async function tick() {
try { render(await go.GetState()); } catch (e) { /* 後端還沒起來,下一輪再試 */ }
}
// ── 動作 ──
$('btnSync').onclick = async () => { await go.SyncNow(); tick(); };
$('btnAddFolder').onclick = async () => {
if (!state || !state.accounts.length) { showConnect(); return; }
const p = await go.PickFolder(); // 系統原生面板(macOS powerbox 會授權)
if (!p) return;
await go.AddFolder(0, p);
tick();
};
function confirmRemove(accIdx, path) {
openSheet(`
<h2>移除這個資料夾?</h2>
<p>「${esc(path)}」不再自動同步。<br/>已經上傳的知識卡不會被刪除。</p>
<div class="acts">
<button id="c1">取消</button>
<button class="primary" id="c2">移除</button>
</div>`, () => {
$('c1').onclick = closeSheet;
$('c2').onclick = async () => { await go.RemoveFolder(accIdx, path); closeSheet(); tick(); };
});
}
function showConnect() {
openSheet(`
<h2>連上你的知識庫</h2>
<p>貼上你的知識庫網址,再輸入你在網站上設定的帳號密碼。</p>
<label>知識庫網址</label>
<input type="text" id="u" placeholder="https://arcrun-cypher-executor.xxxx.workers.dev"/>
<label>帳號(Email</label>
<input type="text" id="e" placeholder="you@example.com"/>
<label>密碼</label>
<input type="password" id="p"/>
<div class="err" id="err" style="display:none"></div>
<div class="acts">
<button id="c1">取消</button>
<button class="primary" id="c2">連線</button>
</div>`, () => {
$('c1').onclick = closeSheet;
$('c2').onclick = async () => {
const err = $('err');
try {
await go.Connect($('u').value.trim(), $('e').value.trim(), $('p').value);
closeSheet(); tick();
} catch (ex) {
err.textContent = String(ex); err.style.display = 'block';
}
};
});
}
$('btnAddAccount').onclick = showConnect;
$('btnAI').onclick = () => {
const gem = state && state.engine === 'gemma';
openSheet(`
<h2>用哪個 AI 幫你整理文件?</h2>
<label class="radio"><input type="radio" name="ai" value="cloud" ${gem ? '' : 'checked'}/>
<span><b>雲端 AI</b>(推薦・不必申請任何金鑰)<br/>
<span class="muted">用你自己 Cloudflare 帳號內建的 AI,不需要任何金鑰。</span></span></label>
<label class="radio"><input type="radio" name="ai" value="gemini" ${gem ? 'checked' : ''}/>
<span><b>Google Gemini</b>(需要自己申請金鑰)</span></label>
<label>Gemini 金鑰${state && state.geminiKey ? '(目前已設定,清空即可刪除)' : ''}</label>
<input type="text" id="k" placeholder="貼上你的 Gemini API Key"/>
<div class="err" id="err" style="display:none"></div>
<div class="acts">
<button id="c1">取消</button>
<button class="primary" id="c2">儲存</button>
</div>`, () => {
$('c1').onclick = closeSheet;
$('c2').onclick = async () => {
const useGemini = document.querySelector('input[name=ai]:checked').value === 'gemini';
const err = $('err');
try {
await go.SetAI(useGemini, $('k').value.trim());
closeSheet(); tick();
} catch (ex) { err.textContent = String(ex); err.style.display = 'block'; }
};
});
};
$('btnUpdate').onclick = () => go.OpenURL('https://rag.arcrun.dev/docs/use/update/');
tick();
setInterval(tick, 1000); // 每秒刷新 ⇒ 同步中看得到在動
+150
View File
@@ -0,0 +1,150 @@
/* Arcrun 桌面 App — 樣式(t193
*
* 🔴 leo 2026-08-04:「我的要求是**符合 CIS**,在風格上**跟 portal 一樣**」
* 「CIS 已經提供規範,你做的**連 Logo 都沒放上去**,這跟美不美有關係嗎?
* 要求放進 CIS 是**硬要求**,你做為檢查有嗎?沒有怎麼交貨?」
*
* ⇒ 本檔的色票/字體/背景紋理**逐字取自 portal**
* matrix/arcrun:console-ui/public/portal/index.html 的 :root 區塊),
* **不自創任何顏色**。要改就去改 portal 那份、再同步過來。
*
* 這也是換掉 fyne 的理由:fyne 全自繪、CSS 套不進去,
* 換成 WailsWebView)後 daemon 與 portal 才可能是**同一套視覺**。
*/
:root {
--paper-a: #FDFCFB; --paper-b: #F2F1ED;
--ink: #17181A; --ink-rgb: 23,24,26;
--amber: #B04A2F; --amber-rgb: 176,74,47;
--ok: #1d7a48; --ok-rgb: 29,122,72;
--err: #b03a26; --err-rgb: 176,58,38;
--well: rgba(23,24,26,.06); --well2: rgba(23,24,26,.08);
}
:root[data-theme="dark"] {
--paper-a: #1b1c1e; --paper-b: #1e1f21;
--ink: #FDFCFB; --ink-rgb: 253,252,251;
--amber: #D9784F; --amber-rgb: 217,120,79;
--ok: #7fe0a8; --ok-rgb: 63,190,120;
--err: #e58575; --err-rgb: 217,95,76;
--well: rgba(0,0,0,.25); --well2: rgba(0,0,0,.3);
}
/* portal 的紙張紋理背景——三站一致的識別(不是裝飾,是 CIS 的一部分) */
html, body {
margin: 0; height: 100%;
background: repeating-linear-gradient(0deg,
var(--paper-a) 0px, var(--paper-a) 3px,
var(--paper-b) 3px, var(--paper-b) 4px);
color: var(--ink);
font-family: -apple-system, "IBM Plex Sans", "PingFang TC", "Noto Sans TC",
"Microsoft JhengHei", system-ui, sans-serif;
font-size: 16px;
-webkit-font-smoothing: antialiased;
overflow: hidden;
}
input, textarea, button, select { font-family: inherit; }
::placeholder { color: rgba(var(--ink-rgb), .35); }
input:focus, select:focus { outline: 2px solid rgba(var(--amber-rgb), .5); outline-offset: 1px; }
a { color: var(--amber); }
.muted { color: rgba(var(--ink-rgb), .55); }
.dim { color: rgba(var(--ink-rgb), .4); }
/* ── 版面:上(品牌+狀態)/中(可捲清單)/下(動作列)── */
#app { display: flex; flex-direction: column; height: 100vh; }
header {
padding: 18px 24px 14px;
border-bottom: 1px solid rgba(var(--ink-rgb), .1);
display: flex; align-items: flex-start; gap: 20px;
}
/* CIS lockup:淺色用 ink 版、深色用 paper 版(與 portal 同一組 PNG、同一套切換規則)。
⚠️ 不可用自產的 SVG 描邊版(字腔會缺,08-01 已作廢,見 arcrun-cis/README.md)。 */
.brand img { height: 26px; display: block; }
.brand .wm-paper { display: none; }
:root[data-theme="dark"] .brand .wm-ink { display: none; }
:root[data-theme="dark"] .brand .wm-paper { display: block; }
.status { flex: 1; min-width: 0; }
.status .big { font-size: 17px; font-weight: 600; letter-spacing: .02em; }
.status .sub { margin-top: 4px; font-size: 13.5px; color: rgba(var(--ink-rgb), .5); }
/* 同步中會呼吸——「看得出來在動」(issue #17) */
.status .big.syncing::after {
content: ""; display: inline-block; width: 7px; height: 7px; margin-left: 9px;
border-radius: 50%; background: var(--amber); vertical-align: middle;
animation: pulse 1.1s ease-in-out infinite;
}
@keyframes pulse { 0%,100% { opacity: .25 } 50% { opacity: 1 } }
/* 按鈕:Relation 是**唯一重點色**,CIS 規定 ≤ 一個畫面的 5% ⇒ 只有主要動作用它 */
button {
border: 1px solid rgba(var(--ink-rgb), .16);
background: transparent; color: var(--ink);
padding: 8px 14px; border-radius: 8px; font-size: 14px; cursor: pointer;
transition: background .12s, border-color .12s;
}
button:hover { background: var(--well); }
button.primary {
background: var(--amber); border-color: var(--amber);
color: #FDFCFB; font-weight: 600;
}
button.primary:hover { filter: brightness(1.06); }
button.ghost { border-color: transparent; color: rgba(var(--ink-rgb), .5); padding: 6px 8px; }
button.ghost:hover { color: var(--err); background: transparent; }
/* ── 中:資料夾清單(leo:「幾十個,根本塞不下」⇒ 這一區才捲動)── */
main { flex: 1; overflow-y: auto; padding: 6px 0; }
.acct {
padding: 16px 24px 6px;
display: flex; align-items: baseline; gap: 10px;
}
.acct .name { font-size: 15px; font-weight: 600; }
.acct .host { font-size: 12.5px; color: rgba(var(--ink-rgb), .4); font-family: ui-monospace, Menlo, monospace; }
.folder {
margin: 0 16px 6px; padding: 11px 14px;
background: var(--well); border-radius: 10px;
display: flex; align-items: center; gap: 12px;
}
.folder .path { flex: 1; min-width: 0; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.folder .tag { font-size: 12px; color: var(--ok); white-space: nowrap; }
/* 空狀態=onboarding(不是空白面板) */
.empty { padding: 56px 32px; text-align: center; }
.empty .t { font-size: 17px; font-weight: 600; margin-bottom: 10px; }
.empty .d { font-size: 14px; color: rgba(var(--ink-rgb), .55); line-height: 1.9; margin-bottom: 22px; }
footer {
padding: 12px 24px; border-top: 1px solid rgba(var(--ink-rgb), .1);
display: flex; align-items: center; gap: 8px;
}
footer .spacer { flex: 1; }
footer .ver { font-size: 12.5px; color: rgba(var(--ink-rgb), .35); font-family: ui-monospace, Menlo, monospace; }
/* 對話框:**內嵌覆蓋層**,不是另開一個小 popup 視窗
(leo:「每個功能都開一個小小的 popup 視窗,非常缺乏整體感」) */
.overlay {
position: fixed; inset: 0; background: rgba(0,0,0,.42);
display: flex; align-items: center; justify-content: center;
opacity: 0; pointer-events: none; transition: opacity .14s;
}
.overlay.on { opacity: 1; pointer-events: auto; }
.sheet {
width: min(520px, calc(100vw - 64px));
background: var(--paper-a); color: var(--ink);
border: 1px solid rgba(var(--ink-rgb), .12);
border-radius: 14px; padding: 24px;
box-shadow: 0 18px 50px rgba(0,0,0,.28);
}
.sheet h2 { margin: 0 0 6px; font-size: 17px; }
.sheet p { margin: 0 0 16px; font-size: 14px; color: rgba(var(--ink-rgb), .55); line-height: 1.7; }
.sheet label { display: block; font-size: 13px; margin: 12px 0 5px; color: rgba(var(--ink-rgb), .6); }
.sheet input[type=text], .sheet input[type=password] {
width: 100%; box-sizing: border-box; padding: 9px 11px; font-size: 14px;
border: 1px solid rgba(var(--ink-rgb), .18); border-radius: 8px;
background: var(--paper-b); color: var(--ink);
}
.sheet .radio { display: flex; align-items: flex-start; gap: 9px; margin: 10px 0; font-size: 14px; cursor: pointer; }
.sheet .radio input { margin-top: 3px; }
.sheet .acts { display: flex; justify-content: flex-end; gap: 8px; margin-top: 22px; }
.sheet .err { color: var(--err); font-size: 13px; margin-top: 10px; }