ework-web 0.10.87 → 0.10.88
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 +1 -1
- package/src/db.ts +4 -0
- package/src/index.ts +29 -1
- package/src/render/layout.ts +9 -1
- package/src/schema-mysql.sql +2 -1
- package/src/schema.sql +4 -1
- package/src/static/issue-actions.js +33 -0
- package/src/store.ts +9 -2
- package/src/views/issueNew.ts +5 -0
- package/src/views/issueThread.ts +1 -0
- package/src/webhooks.ts +11 -4
package/package.json
CHANGED
package/src/db.ts
CHANGED
|
@@ -134,6 +134,9 @@ function migrateIssuesTable(db: Database): void {
|
|
|
134
134
|
if (!have.has("model")) {
|
|
135
135
|
db.exec(applyPrefix("ALTER TABLE {{issues}} ADD COLUMN model TEXT NOT NULL DEFAULT ''"));
|
|
136
136
|
}
|
|
137
|
+
if (!have.has("runtime")) {
|
|
138
|
+
db.exec(applyPrefix("ALTER TABLE {{issues}} ADD COLUMN runtime TEXT NOT NULL DEFAULT ''"));
|
|
139
|
+
}
|
|
137
140
|
if (!have.has("upstream_issue_number")) {
|
|
138
141
|
db.exec(applyPrefix("ALTER TABLE {{issues}} ADD COLUMN upstream_issue_number INTEGER"));
|
|
139
142
|
}
|
|
@@ -391,6 +394,7 @@ async function migrateMysqlColumn(pool: Pool, table: string, column: string, ddl
|
|
|
391
394
|
async function migrateMysqlIssuesAiStatus(pool: Pool): Promise<void> {
|
|
392
395
|
await migrateMysqlColumn(pool, "issues", "ai_status", "ai_status VARCHAR(32) NOT NULL DEFAULT ''");
|
|
393
396
|
await migrateMysqlColumn(pool, "issues", "model", "model VARCHAR(128) NOT NULL DEFAULT ''");
|
|
397
|
+
await migrateMysqlColumn(pool, "issues", "runtime", "runtime VARCHAR(32) NOT NULL DEFAULT ''");
|
|
394
398
|
await migrateMysqlColumn(pool, "issues", "upstream_issue_number", "upstream_issue_number INT DEFAULT NULL");
|
|
395
399
|
await migrateMysqlColumn(pool, "comments", "model", "model VARCHAR(128) NOT NULL DEFAULT ''");
|
|
396
400
|
await migrateMysqlColumn(pool, "comments", "upstream_comment_id", "upstream_comment_id BIGINT DEFAULT NULL");
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
setIssueState,
|
|
41
41
|
updateIssueAiStatus,
|
|
42
42
|
updateIssueModel,
|
|
43
|
+
updateIssueRuntime,
|
|
43
44
|
listIssues,
|
|
44
45
|
createAttachment,
|
|
45
46
|
getAttachment,
|
|
@@ -398,6 +399,7 @@ const REPO_LABEL_ADD_RE = /^\/([^/]+)\/([^/]+)\/settings\/labels\/add$/;
|
|
|
398
399
|
const REPO_LABEL_ACTION_RE = /^\/([^/]+)\/([^/]+)\/settings\/labels\/(\d+)\/(update|archive|unarchive|delete)$/;
|
|
399
400
|
const REPO_ISSUE_HALT_RE = /^\/([^/]+)\/([^/]+)\/issues\/(\d+)\/(halt|resume|dispatch-off|dispatch-on)$/;
|
|
400
401
|
const REPO_ISSUE_MODEL_RE = /^\/([^/]+)\/([^/]+)\/issues\/(\d+)\/model$/;
|
|
402
|
+
const REPO_ISSUE_RUNTIME_RE = /^\/([^/]+)\/([^/]+)\/issues\/(\d+)\/runtime$/;
|
|
401
403
|
const REPO_ISSUE_STATUS_RE = /^\/([^/]+)\/([^/]+)\/issues\/(\d+)\/ai-status$/;
|
|
402
404
|
const API_ISSUE_LABELS_RE = /^\/api\/([^/]+)\/([^/]+)\/issues\/(\d+)\/labels$/;
|
|
403
405
|
const WH_ACTION_RE = /^\/__wh\/(\d+)\/(delete|toggle|test)$/;
|
|
@@ -1903,6 +1905,28 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
1903
1905
|
}
|
|
1904
1906
|
}
|
|
1905
1907
|
|
|
1908
|
+
const rt = url.pathname.match(REPO_ISSUE_RUNTIME_RE);
|
|
1909
|
+
if (rt) {
|
|
1910
|
+
const [, owner, repo, numStr] = rt;
|
|
1911
|
+
if (!(owner && repo && numStr)) return json({ error: "bad path" }, 400);
|
|
1912
|
+
const number = Number(numStr);
|
|
1913
|
+
try {
|
|
1914
|
+
const project = await getProject(owner, repo);
|
|
1915
|
+
if (!project) return json({ error: "project not found" }, 404);
|
|
1916
|
+
const issue = await getIssueWithMeta(project.id, number);
|
|
1917
|
+
if (!issue) return json({ error: "issue not found" }, 404);
|
|
1918
|
+
if (!(await canWriteProject(project.id, ctx.user))) {
|
|
1919
|
+
return json({ error: "writer role required" }, 403);
|
|
1920
|
+
}
|
|
1921
|
+
const form = await req.formData().catch(() => new FormData());
|
|
1922
|
+
const runtime = String(form.get("runtime") ?? "").trim().slice(0, 32);
|
|
1923
|
+
await updateIssueRuntime(issue.id, runtime);
|
|
1924
|
+
return json({ ok: true, runtime: runtime === "pi" || runtime === "opencode" ? runtime : "" });
|
|
1925
|
+
} catch (e) {
|
|
1926
|
+
return json({ error: errMsg(e) }, e instanceof StoreError ? e.status : 500);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1906
1930
|
const ci = url.pathname.match(REPO_LIST_RE);
|
|
1907
1931
|
if (ci) {
|
|
1908
1932
|
const [, owner, repo] = ci;
|
|
@@ -1926,7 +1950,11 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
|
|
|
1926
1950
|
await ensureProjectBootstrapAdmin(project.id, ctx.user!.login);
|
|
1927
1951
|
await autoWireDaemon(project.id, url.origin);
|
|
1928
1952
|
}
|
|
1929
|
-
const
|
|
1953
|
+
const formRuntime = String(form.get("runtime") ?? "").trim();
|
|
1954
|
+
const issueOpts: { model?: string; runtime?: string } = {};
|
|
1955
|
+
if (model) issueOpts.model = model;
|
|
1956
|
+
if (formRuntime === "pi" || formRuntime === "opencode") issueOpts.runtime = formRuntime;
|
|
1957
|
+
const issue = await createIssue(project.id, title, body, ctx.user!.login, issueOpts);
|
|
1930
1958
|
void emitIssueEvent(project.id, issue.id, "opened", url.origin);
|
|
1931
1959
|
return Response.redirect(
|
|
1932
1960
|
`${url.origin}/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${issue.number}`,
|
package/src/render/layout.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface LayoutProps {
|
|
|
24
24
|
customActions?: IssueAction[];
|
|
25
25
|
extraStatusBadges?: Record<string, { cls: string; label: string }>;
|
|
26
26
|
modelSelect?: { current: string; options: { id: string; label: string }[] } | null;
|
|
27
|
+
runtimeSelect?: { current: string } | null;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export const THEME_CSS = `
|
|
@@ -206,6 +207,13 @@ export function renderLayout(props: LayoutProps, inner: string, initialItems: st
|
|
|
206
207
|
? `<button type="button" class="action-btn dispatch-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/dispatch-on" title="允许自动接单">🔔 恢复接单</button>`
|
|
207
208
|
: `<button type="button" class="action-btn dispatch-btn" data-action-href="${escapeAttr(repoIssuesHref)}/${props.issueNumber}/dispatch-off" data-action-confirm="设为不自动接单?" title="关闭此 issue 的自动接单">🔕 暂停接单</button>`
|
|
208
209
|
: "";
|
|
210
|
+
const runtimeSelectHtml = props.runtimeSelect && showActions
|
|
211
|
+
? `<span class="model-select-wrap"><select class="model-select" id="issueRuntimeSelect" title="此 issue 的运行时(新会话生效)">
|
|
212
|
+
<option value="" ${props.runtimeSelect.current === "" ? "selected" : ""}>默认运行时</option>
|
|
213
|
+
<option value="opencode" ${props.runtimeSelect.current === "opencode" ? "selected" : ""}>opencode</option>
|
|
214
|
+
<option value="pi" ${props.runtimeSelect.current === "pi" ? "selected" : ""}>pi</option>
|
|
215
|
+
</select><button type="button" class="action-btn model-save-btn" id="issueRuntimeSave" title="保存运行时选择">💾</button></span>`
|
|
216
|
+
: "";
|
|
209
217
|
const modelSelectHtml = props.modelSelect && props.modelSelect.options.length > 0 && showActions
|
|
210
218
|
? `<span class="model-select-wrap"><select class="model-select" id="issueModelSelect" title="此 issue 的模型(覆盖项目/全局默认)">
|
|
211
219
|
<option value="">默认模型</option>
|
|
@@ -234,7 +242,7 @@ export function renderLayout(props: LayoutProps, inner: string, initialItems: st
|
|
|
234
242
|
<div class="meta-status">
|
|
235
243
|
<span class="state-badge ${stateClass}">${stateLabel}</span>
|
|
236
244
|
${aiBadgeHtml}
|
|
237
|
-
${(haltBtnHtml || dispatchBtnHtml || (props.customActions ?? []).length) ? `<span class="action-group">${haltBtnHtml}${dispatchBtnHtml}${modelSelectHtml}${(props.customActions ?? []).map((a) => {
|
|
245
|
+
${(haltBtnHtml || dispatchBtnHtml || (props.customActions ?? []).length) ? `<span class="action-group">${haltBtnHtml}${dispatchBtnHtml}${runtimeSelectHtml}${modelSelectHtml}${(props.customActions ?? []).map((a) => {
|
|
238
246
|
const attrs = [`data-action-href="${escapeAttr(a.href)}"`];
|
|
239
247
|
if (a.method && a.method !== "POST") attrs.push(`data-action-method="${escapeAttr(a.method)}"`);
|
|
240
248
|
if (a.confirm) attrs.push(`data-action-confirm="${escapeAttr(a.confirm)}"`);
|
package/src/schema-mysql.sql
CHANGED
|
@@ -55,6 +55,7 @@ CREATE TABLE IF NOT EXISTS {{issues}} (
|
|
|
55
55
|
closed_at VARCHAR(40) DEFAULT NULL,
|
|
56
56
|
ai_status VARCHAR(32) NOT NULL DEFAULT '',
|
|
57
57
|
model VARCHAR(128) NOT NULL DEFAULT '',
|
|
58
|
+
runtime VARCHAR(32) NOT NULL DEFAULT '',
|
|
58
59
|
upstream_issue_number INT DEFAULT NULL,
|
|
59
60
|
UNIQUE (project_id, number),
|
|
60
61
|
UNIQUE uq_issues_project_upstream (project_id, upstream_issue_number),
|
|
@@ -78,7 +79,7 @@ CREATE TABLE IF NOT EXISTS {{comments}} (
|
|
|
78
79
|
UNIQUE uq_comments_upstream (upstream_comment_id),
|
|
79
80
|
CONSTRAINT {{fk_comments_issue}} FOREIGN KEY (issue_id) REFERENCES {{issues}}(id) ON DELETE CASCADE,
|
|
80
81
|
CONSTRAINT {{fk_comments_author}} FOREIGN KEY (author) REFERENCES {{users}}(login),
|
|
81
|
-
model VARCHAR(128) NOT NULL DEFAULT ''
|
|
82
|
+
model VARCHAR(128) NOT NULL DEFAULT '',
|
|
82
83
|
CREATE INDEX comments_issue_created ON {{comments}} (issue_id, created_at);
|
|
83
84
|
CREATE INDEX comments_author ON {{comments}} (author);
|
|
84
85
|
|
package/src/schema.sql
CHANGED
|
@@ -67,6 +67,8 @@ CREATE TABLE IF NOT EXISTS {{issues}} (
|
|
|
67
67
|
ai_status TEXT NOT NULL DEFAULT '',
|
|
68
68
|
-- Resolved "provider/model" for this issue. Empty = inherit project/global default.
|
|
69
69
|
model TEXT NOT NULL DEFAULT '',
|
|
70
|
+
-- Runtime backend pinned for this issue ('' = daemon default, 'opencode', 'pi').
|
|
71
|
+
runtime TEXT NOT NULL DEFAULT '',
|
|
70
72
|
-- Upstream Gitea issue number this row was imported from (NULL = native).
|
|
71
73
|
upstream_issue_number INTEGER,
|
|
72
74
|
UNIQUE (project_id, number)
|
|
@@ -86,7 +88,8 @@ CREATE TABLE IF NOT EXISTS {{comments}} (
|
|
|
86
88
|
created_at TEXT NOT NULL,
|
|
87
89
|
updated_at TEXT NOT NULL DEFAULT '',
|
|
88
90
|
upstream_comment_id INTEGER,
|
|
89
|
-
model TEXT NOT NULL DEFAULT ''
|
|
91
|
+
model TEXT NOT NULL DEFAULT '',
|
|
92
|
+
runtime TEXT NOT NULL DEFAULT ''
|
|
90
93
|
);
|
|
91
94
|
CREATE INDEX IF NOT EXISTS comments_issue_created
|
|
92
95
|
ON {{comments}} (issue_id, created_at);
|
|
@@ -115,4 +115,37 @@
|
|
|
115
115
|
document.addEventListener("change", function (e) {
|
|
116
116
|
if (e.target && e.target.id === "issueModelSelect") saveModel(document.getElementById("issueModelSave"));
|
|
117
117
|
});
|
|
118
|
+
|
|
119
|
+
function saveRuntime(saveBtn) {
|
|
120
|
+
var sel = document.getElementById("issueRuntimeSelect");
|
|
121
|
+
if (!sel || !saveBtn) return;
|
|
122
|
+
saveBtn.disabled = true;
|
|
123
|
+
var path = location.pathname.split("/").slice(0, 5).join("/");
|
|
124
|
+
fetch(path + "/runtime", {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
127
|
+
body: "runtime=" + encodeURIComponent(sel.value)
|
|
128
|
+
})
|
|
129
|
+
.then(function (r) { return r.json(); })
|
|
130
|
+
.then(function (d) {
|
|
131
|
+
saveBtn.disabled = false;
|
|
132
|
+
if (d.ok) { saveBtn.textContent = "✓"; setTimeout(function () { saveBtn.textContent = "💾"; }, 1200); }
|
|
133
|
+
else alert(d.error || "保存失败");
|
|
134
|
+
})
|
|
135
|
+
.catch(function (err) {
|
|
136
|
+
saveBtn.disabled = false;
|
|
137
|
+
alert("网络错误: " + err);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
document.addEventListener("click", function (e) {
|
|
142
|
+
var saveBtn = e.target.closest("#issueRuntimeSave");
|
|
143
|
+
if (!saveBtn) return;
|
|
144
|
+
e.preventDefault();
|
|
145
|
+
saveRuntime(saveBtn);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
document.addEventListener("change", function (e) {
|
|
149
|
+
if (e.target && e.target.id === "issueRuntimeSelect") saveRuntime(document.getElementById("issueRuntimeSave"));
|
|
150
|
+
});
|
|
118
151
|
})();
|
package/src/store.ts
CHANGED
|
@@ -75,6 +75,7 @@ export interface IssueRow {
|
|
|
75
75
|
closed_at: string | null;
|
|
76
76
|
ai_status: string;
|
|
77
77
|
model: string;
|
|
78
|
+
runtime: string;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
export interface IssueWithMeta extends IssueRow {
|
|
@@ -453,6 +454,7 @@ export interface CreateIssueOpts {
|
|
|
453
454
|
state?: "open" | "closed";
|
|
454
455
|
closedAt?: string | null;
|
|
455
456
|
model?: string;
|
|
457
|
+
runtime?: string;
|
|
456
458
|
upstreamIssueNumber?: number;
|
|
457
459
|
}
|
|
458
460
|
|
|
@@ -484,8 +486,8 @@ export async function createIssue(
|
|
|
484
486
|
"SELECT COALESCE(MAX(number), 0) + 1 AS n FROM {{issues}} WHERE project_id = ?", [projectId]
|
|
485
487
|
))!;
|
|
486
488
|
const info = await getDB().run(
|
|
487
|
-
"INSERT INTO {{issues}} (project_id, number, title, body, state, author, created_at, updated_at, closed_at, model, upstream_issue_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
488
|
-
[projectId, next.n, title, body, state, author, createdAt, updatedAt, closedAt, opts.model ?? "", opts.upstreamIssueNumber ?? null]
|
|
489
|
+
"INSERT INTO {{issues}} (project_id, number, title, body, state, author, created_at, updated_at, closed_at, model, runtime, upstream_issue_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
490
|
+
[projectId, next.n, title, body, state, author, createdAt, updatedAt, closedAt, opts.model ?? "", opts.runtime ?? "", opts.upstreamIssueNumber ?? null]
|
|
489
491
|
);
|
|
490
492
|
await getDB().run("UPDATE {{projects}} SET updated_at = ? WHERE id = ?", [updatedAt, projectId]);
|
|
491
493
|
return (await getIssueById(info.insertId))!;
|
|
@@ -525,6 +527,11 @@ export async function updateIssueModel(issueId: number, model: string): Promise<
|
|
|
525
527
|
await getDB().run("UPDATE {{issues}} SET model = ?, updated_at = ? WHERE id = ?", [clean, now(), issueId]);
|
|
526
528
|
}
|
|
527
529
|
|
|
530
|
+
export async function updateIssueRuntime(issueId: number, runtime: string): Promise<void> {
|
|
531
|
+
const v = runtime === "pi" || runtime === "opencode" ? runtime : "";
|
|
532
|
+
await getDB().run("UPDATE {{issues}} SET runtime = ? WHERE id = ?", [v, issueId]);
|
|
533
|
+
}
|
|
534
|
+
|
|
528
535
|
export interface UpstreamSyncRow {
|
|
529
536
|
id: number;
|
|
530
537
|
project_id: number;
|
package/src/views/issueNew.ts
CHANGED
|
@@ -15,6 +15,11 @@ export function buildIssueNew(owner: string, repo: string, writesEnabled: boolea
|
|
|
15
15
|
<input type="text" name="title" placeholder="标题(必填)" required maxlength="255" class="new-title">
|
|
16
16
|
<textarea name="body" rows="14" placeholder="正文(支持 Markdown)…"></textarea>
|
|
17
17
|
${modelSelect}
|
|
18
|
+
<select name="runtime" class="new-model">
|
|
19
|
+
<option value="">默认运行时(daemon 设置)</option>
|
|
20
|
+
<option value="opencode">opencode</option>
|
|
21
|
+
<option value="pi">pi</option>
|
|
22
|
+
</select>
|
|
18
23
|
<div class="new-actions"><a class="new-cancel" href="${escapeAttr(listHref)}">取消</a><button type="submit">创建工单</button></div>
|
|
19
24
|
</form>`
|
|
20
25
|
: `<div class="composer-ro">只读模式:创建工单未启用(WORK_WRITES_ENABLED=false)</div>`;
|
package/src/views/issueThread.ts
CHANGED
|
@@ -161,6 +161,7 @@ export async function buildIssueThread(
|
|
|
161
161
|
modelSelect: cfg.writesEnabled !== false
|
|
162
162
|
? { current: issue.model ?? "", options: (await listCachedModels()).map((m) => ({ id: m.id, label: m.label })) }
|
|
163
163
|
: null,
|
|
164
|
+
runtimeSelect: cfg.writesEnabled !== false ? { current: issue.runtime ?? "" } : null,
|
|
164
165
|
},
|
|
165
166
|
safeJsonEmbed(payload),
|
|
166
167
|
displayViews.map((v) => renderCommentCard(v, cfg)).join("")
|
package/src/webhooks.ts
CHANGED
|
@@ -269,6 +269,7 @@ interface PayloadRepository {
|
|
|
269
269
|
// `--model <X>` on opencode spawn. Empty string = no override (let opencode
|
|
270
270
|
// pick). Gitea-strict consumers ignore unknown fields per JSON POST rules.
|
|
271
271
|
ework_model?: string;
|
|
272
|
+
ework_runtime?: string;
|
|
272
273
|
}
|
|
273
274
|
|
|
274
275
|
interface PayloadIssue {
|
|
@@ -364,7 +365,12 @@ function buildUserFromRow(user: UserRow, origin: string): PayloadUser {
|
|
|
364
365
|
};
|
|
365
366
|
}
|
|
366
367
|
|
|
367
|
-
function buildRepository(
|
|
368
|
+
function buildRepository(
|
|
369
|
+
project: ProjectRow,
|
|
370
|
+
origin: string,
|
|
371
|
+
model?: string,
|
|
372
|
+
runtime?: string,
|
|
373
|
+
): PayloadRepository {
|
|
368
374
|
const fullName = `${project.owner}/${project.name}`;
|
|
369
375
|
const htmlUrl = `${origin}/${encodeURIComponent(project.owner)}/${encodeURIComponent(project.name)}`;
|
|
370
376
|
// clone_url must be a real Git remote (ework-web is NOT a Git server). Use the
|
|
@@ -399,6 +405,7 @@ function buildRepository(project: ProjectRow, origin: string, model?: string): P
|
|
|
399
405
|
// Only attach ework_model when non-empty (keeps payload compact + lets
|
|
400
406
|
// Gitea-strict consumers ignore the field entirely on no-op cases).
|
|
401
407
|
if (model) repo.ework_model = model;
|
|
408
|
+
if (runtime) repo.ework_runtime = runtime;
|
|
402
409
|
return repo;
|
|
403
410
|
}
|
|
404
411
|
|
|
@@ -436,7 +443,7 @@ function buildIssue(
|
|
|
436
443
|
closed_at: issue.closed_at,
|
|
437
444
|
due_date: null,
|
|
438
445
|
pull_request: null,
|
|
439
|
-
repository: buildRepository(project, origin, model),
|
|
446
|
+
repository: buildRepository(project, origin, model, issue.runtime || undefined),
|
|
440
447
|
user: buildUser(issue.author, origin),
|
|
441
448
|
ai_status: issue.ai_status ?? "",
|
|
442
449
|
};
|
|
@@ -475,7 +482,7 @@ function buildCommentPayload(
|
|
|
475
482
|
action: "created",
|
|
476
483
|
issue: buildIssue(issue, project, commentCount, origin, model, labels),
|
|
477
484
|
comment: buildComment(issue, comment, project, origin),
|
|
478
|
-
repository: buildRepository(project, origin, model),
|
|
485
|
+
repository: buildRepository(project, origin, model, issue.runtime || undefined),
|
|
479
486
|
sender: buildUser(comment.author, origin),
|
|
480
487
|
};
|
|
481
488
|
}
|
|
@@ -492,7 +499,7 @@ function buildIssuePayload(
|
|
|
492
499
|
return {
|
|
493
500
|
action,
|
|
494
501
|
issue: buildIssue(issue, project, commentCount, origin, model, labels),
|
|
495
|
-
repository: buildRepository(project, origin, model),
|
|
502
|
+
repository: buildRepository(project, origin, model, issue.runtime || undefined),
|
|
496
503
|
sender: buildUser(issue.author, origin),
|
|
497
504
|
};
|
|
498
505
|
}
|