ework-web 0.10.79 → 0.10.81

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-web",
3
- "version": "0.10.79",
3
+ "version": "0.10.81",
4
4
  "type": "module",
5
5
  "description": "ework-web — standalone multi-project issue tracker. Local SQLite-backed, no external API dependency. Bun + TypeScript + SSR HTML.",
6
6
  "license": "MIT",
@@ -75,6 +75,30 @@ export interface SessionDaemonInfo {
75
75
  endpoint: string;
76
76
  }
77
77
 
78
+ export interface SessionUidInfo {
79
+ daemonId: number;
80
+ opencodeSessionId: string | null;
81
+ }
82
+
83
+ export async function resolveSessionUid(uid: string): Promise<SessionUidInfo | null> {
84
+ try {
85
+ const rows = await getDB().all<{ daemon_id: number; opencode_session_id: string | null }>(
86
+ `SELECT i.owner_daemon_id AS daemon_id, s.opencode_session_id
87
+ FROM {{d_op_sessions}} s
88
+ JOIN {{d_issues}} i ON i.uid = s.issue_id
89
+ WHERE s.uid = ?
90
+ LIMIT 1`,
91
+ [uid],
92
+ );
93
+ const r = rows[0];
94
+ if (!r || !r.daemon_id) return null;
95
+ return { daemonId: r.daemon_id, opencodeSessionId: r.opencode_session_id && r.opencode_session_id !== "" ? r.opencode_session_id : null };
96
+ } catch (e) {
97
+ log.info(`coordination: session uid resolve failed (${e instanceof Error ? e.message : String(e)})`);
98
+ return null;
99
+ }
100
+ }
101
+
78
102
  export async function getSessionDaemonMap(): Promise<Map<string, SessionDaemonInfo>> {
79
103
  try {
80
104
  const rows = await getDB().all<{
package/src/index.ts CHANGED
@@ -6,7 +6,7 @@ import { homedir } from "os";
6
6
  import { loadConfig, DB_OVERRIDABLE, parseOverride, resolveTtsBackend } from "./config";
7
7
  import type { Config } from "./config";
8
8
  import { setConfig, deleteConfig, getConfigAll, initDB, getDB } from "./db";
9
- import { getActiveDaemons, listAllDaemons, getSessionDaemonMap, resolveDaemonEndpoint, getRunningSessionsForProject } from "./coordination";
9
+ import { getActiveDaemons, listAllDaemons, getSessionDaemonMap, resolveDaemonEndpoint, getRunningSessionsForProject, resolveSessionUid } from "./coordination";
10
10
  import { testMysqlConnection, migrateSqliteToMysql, writeMysqlEnv, migrateMysqlToSqlite, writeSqliteEnv, migrateDaemonSqliteToMysql, generateMysqlDDL } from "./db-admin";
11
11
  import type { MysqlTargetOpts } from "./db-admin";
12
12
  import { checkAuth, makeAuthCookieHeader, clearAuthCookieHeader, loginHTML, sanitizeNext, ensureBootstrapAdmin, ensureBootstrapSystem, isReservedSystemLogin } from "./auth";
@@ -661,7 +661,7 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
661
661
  }
662
662
 
663
663
  if (url.pathname === "/") {
664
- return Response.redirect(`${url.origin}/projects`, 302);
664
+ return Response.redirect(`${url.origin}/issues`, 302);
665
665
  }
666
666
 
667
667
  if (url.pathname === "/projects") {
@@ -709,8 +709,22 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
709
709
 
710
710
  const sessionView = url.pathname.match(SESSION_VIEW_RE);
711
711
  if (sessionView) {
712
- const [, sid] = sessionView;
713
- if (!sid) return html(errorPage("404", "bad session path"), 404);
712
+ const [, rawSid] = sessionView;
713
+ if (!rawSid) return html(errorPage("404", "bad session path"), 404);
714
+ // Daemon-internal session UIDs (36-char uuid) appear in pickup acks before
715
+ // the backend reports its opencode id: redirect once known, hold page if not.
716
+ let sid = rawSid;
717
+ if (!sid.startsWith("ses_")) {
718
+ const uidInfo = await resolveSessionUid(rawSid);
719
+ if (uidInfo?.opencodeSessionId) {
720
+ const qs = new URLSearchParams(url.searchParams);
721
+ qs.set("daemon_id", String(uidInfo.daemonId));
722
+ return Response.redirect(`${url.origin}/sessions/${encodeURIComponent(uidInfo.opencodeSessionId)}?${qs}`, 302);
723
+ }
724
+ if (uidInfo) {
725
+ return html(errorPage("等待启动", "会话已创建,后端会话 ID 尚未生成(正在准备工作目录)。稍后刷新此页。"), 200);
726
+ }
727
+ }
714
728
  const desc = url.searchParams.get("asc") !== "1";
715
729
  const all = url.searchParams.get("all") === "1";
716
730
  const limit = Math.min(5000, Math.max(1, Number(url.searchParams.get("limit")) || 30));
package/src/views/home.ts CHANGED
@@ -70,7 +70,7 @@ export async function buildHome(
70
70
  .flash.ok{background:rgba(63,185,80,.12);color:#3fb950;border-color:rgba(63,185,80,.4)}
71
71
  .flash.err{background:rgba(248,81,73,.12);color:#f85149;border-color:rgba(248,81,73,.4)}
72
72
  </style></head><body>
73
- <header class="topbar"><a href="/" style="font-weight:600;color:var(--header-text)">🏠 ework</a></header>
73
+ <header class="topbar"><a href="/" title="ework 主页" style="color:var(--header-text)">🏠</a></header>
74
74
  ${tabNavHTML("projects", viewer ? { login: viewer.login, is_admin: viewer.is_admin } : undefined)}
75
75
  <main class="home-wrap">
76
76
  ${flashHtml}
@@ -73,7 +73,7 @@ export async function buildIssuesFeed(
73
73
  .label-filter{font-size:13px;color:var(--text-muted);margin-bottom:.4rem}
74
74
  .label-filter strong{color:var(--text)}
75
75
  </style></head><body>
76
- <header class="topbar"><a href="/" style="font-weight:600;color:var(--header-text)">🏠 ework</a></header>
76
+ <header class="topbar"><a href="/" title="ework 主页" style="color:var(--header-text)">🏠</a></header>
77
77
  ${tabNavHTML("issues", viewer ? { login: viewer.login, is_admin: viewer.is_admin } : undefined)}
78
78
  <main class="feed-wrap">
79
79
  <form class="search" method="GET" action="/issues">
@@ -71,7 +71,7 @@ button.danger{background:#da3633;color:#fff}
71
71
  button:disabled{opacity:.5;cursor:not-allowed}
72
72
  .future{opacity:.5;pointer-events:none}
73
73
  </style></head><body>
74
- <header class="nav"><a href="/" style="color:var(--header-text)">🏠 ework-web</a><span style="opacity:.8"> · ${escapeHtml(project.owner + "/" + project.name)}</span></header>
74
+ <header class="nav"><a href="/" title="ework 主页" style="color:var(--header-text)">🏠</a><span style="opacity:.8"> · ${escapeHtml(project.owner + "/" + project.name)}</span></header>
75
75
  <main class="wrap">
76
76
  ${projectSettingsTabsHTML(project.owner, project.name, "ai")}
77
77
  <h1>🤖 AI 设置</h1>
@@ -60,7 +60,7 @@ button{padding:.5rem 1.2rem;border:0;border-radius:6px;background:var(--accent);
60
60
  .flash.ok{background:#1f6feb;color:#fff}
61
61
  .flash.err{background:#da3633;color:#fff}
62
62
  </style></head><body>
63
- <header class="nav"><a href="/" style="color:var(--header-text)">🏠 ework-web</a><span style="opacity:.8"> · ${escapeHtml(project.owner + "/" + project.name)}</span></header>
63
+ <header class="nav"><a href="/" title="ework 主页" style="color:var(--header-text)">🏠</a><span style="opacity:.8"> · ${escapeHtml(project.owner + "/" + project.name)}</span></header>
64
64
  <main class="wrap">
65
65
  ${projectSettingsTabsHTML(project.owner, project.name, "model")}
66
66
  <h1>🤖 模型设置</h1>
@@ -48,7 +48,7 @@ export async function buildSessionList(
48
48
  .sd-badge{background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:0 .3rem;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px}
49
49
  .empty{padding:2rem;text-align:center;color:var(--text-muted)}
50
50
  </style></head><body>
51
- <header class="nav" style="display:flex;align-items:center;gap:.5rem;padding:.55rem 1rem;background:var(--header-bg);color:var(--header-text);font-size:13px"><a href="/" style="color:var(--header-text)">🏠 ework-web</a><span style="opacity:.8"> · OpenCode 会话 ${sessions.length}</span></header>
51
+ <header class="nav" style="display:flex;align-items:center;gap:.5rem;padding:.55rem 1rem;background:var(--header-bg);color:var(--header-text);font-size:13px"><a href="/" title="ework 主页" style="color:var(--header-text)">🏠</a><span style="opacity:.8"> · OpenCode 会话 ${sessions.length}</span></header>
52
52
  ${tabNavHTML("sessions", viewer ? { login: viewer.login, is_admin: viewer.is_admin } : undefined)}
53
53
  <form class="sbar" method="get" action="/sessions">
54
54
  <input name="q" value="${escapeAttr(q)}" placeholder="按标题或会话 ID 搜索…" autocomplete="off">
@@ -165,7 +165,7 @@ button.secondary{background:transparent;color:var(--text-muted);border:1px solid
165
165
  .daemon-list button.remove{padding:.25rem .6rem;font-size:11px;background:transparent;color:var(--text-muted);border:1px solid var(--border)}
166
166
  .daemon-empty{color:var(--text-muted);font-size:13px;padding:.5rem 0}
167
167
  </style></head><body>
168
- <header class="nav"><a href="/" style="color:var(--header-text)">🏠 ework-web</a><span style="opacity:.8"> · 设置</span><span style="margin-left:auto;opacity:.85">👤 ${escapeHtml(viewer.login)}</span><form method="post" action="/logout" style="display:inline"><button type="submit" style="background:none;border:1px solid rgba(255,255,255,.3);color:var(--header-text);border-radius:6px;padding:.2rem .7rem;font-size:12px;cursor:pointer;font:inherit">退出</button></form></header>
168
+ <header class="nav"><a href="/" title="ework 主页" style="color:var(--header-text)">🏠</a><span style="opacity:.8"> · 设置</span><span style="margin-left:auto;opacity:.85">👤 ${escapeHtml(viewer.login)}</span><form method="post" action="/logout" style="display:inline"><button type="submit" style="background:none;border:1px solid rgba(255,255,255,.3);color:var(--header-text);border-radius:6px;padding:.2rem .7rem;font-size:12px;cursor:pointer;font:inherit">退出</button></form></header>
169
169
  <main class="wrap">
170
170
  <h1>后台配置</h1>
171
171
  <p class="hint">改完保存立即生效,无需重启。密钥/启动项(token、端口等)仍在 <code>.env</code>,不在此处。</p>
@@ -79,7 +79,7 @@ h1{font-size:18px;margin:0 0 .3rem}
79
79
  .tab{padding:.3rem .7rem;border-radius:6px 6px 0 0;text-decoration:none;color:var(--text-muted)}
80
80
  .tab.active{background:var(--accent);color:#fff}
81
81
  </style></head><body>
82
- <header class="nav"><a href="/" style="color:var(--header-text)">🏠 ework-web</a></header>
82
+ <header class="nav"><a href="/" title="ework 主页" style="color:var(--header-text)">🏠</a></header>
83
83
  ${tabNavHTML("projects", viewer)}
84
84
  <main class="wrap">
85
85
  <h1>Webhook 投递记录</h1>
@@ -142,7 +142,7 @@ button[type=submit]{padding:.5rem 1.2rem;border:0;border-radius:6px;background:v
142
142
  .subtab{padding:.35rem .8rem;border-radius:6px 6px 0 0;font-size:13px;color:var(--text-muted)}
143
143
  .subtab.active{background:var(--bg-muted);color:var(--text);font-weight:600}
144
144
  </style></head><body>
145
- <header class="nav"><a href="/" style="color:var(--header-text)">🏠 ework-web</a></header>
145
+ <header class="nav"><a href="/" title="ework 主页" style="color:var(--header-text)">🏠</a></header>
146
146
  ${tabNavHTML("projects")}
147
147
  <main class="wrap">
148
148
  <div class="crumb"><a href="/projects">项目</a> · <a href="/${encodeURIComponent(project.owner)}/${encodeURIComponent(project.name)}">${escapeHtml(project.owner + "/" + project.name)}</a> · 设置</div>