From ae5e08869fe8e3943a978a8fbd4b52a0f7dfa263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 28 May 2026 20:21:51 -0400 Subject: [PATCH] Extract chat-service internals into a module chat-service.ts exceeded 500 lines. Move the row-shape interfaces, slash-command parsing/validation and the small standalone helpers into chat-service-internal.ts and import them back. Pure relocation with no behaviour change; the chat governance suite (82 tests) and e2e remain green. --- .../main/services/chat-service-internal.ts | 105 ++++++++++++++++ .../src/main/services/chat-service.ts | 119 +++--------------- 2 files changed, 122 insertions(+), 102 deletions(-) create mode 100644 AgentCompany-New/src/main/services/chat-service-internal.ts diff --git a/AgentCompany-New/src/main/services/chat-service-internal.ts b/AgentCompany-New/src/main/services/chat-service-internal.ts new file mode 100644 index 00000000..20442d53 --- /dev/null +++ b/AgentCompany-New/src/main/services/chat-service-internal.ts @@ -0,0 +1,105 @@ +import type { DatabaseSync } from "node:sqlite"; +import type { CreateChatSpaceInput, SendMessageInput } from "../../shared/commands"; +import type { FileScanState } from "../../shared/domain"; +import { parseBoardCommand } from "./board-intervention-service"; + +export const RETENTION_POLICIES = new Set(["standard_365d", "restricted_365d"]); + +export const ALLOWED_SLASH_COMMANDS = new Set([ + "agent", + "handoff", + "approve", + "deny", + "skill", + "runbook", + "file", + "escalate", + "board", +]); + +export interface FileAttachmentRow { + id: string; + sha256: string; + scan_state: FileScanState; + scan_labels_json: string; + storage_path: string; + owner_type: string; + owner_id: string | null; +} + +export interface SpaceRow { + id: string; + org_id: string; + visibility: string; + space_type: string; +} + +export interface ThreadRow { + id: string; + board_state: string; + controlled_by_type: "employee" | "agent" | "board" | null; + controlled_by_id: string | null; +} + +export interface PrincipalRef { + type: "employee" | "agent"; + id: string; +} + +export interface SlashInvocation { + command: string; + args: string; +} + +export interface ApprovalRequester { + type: "employee" | "agent" | "system"; + id: string | null; +} + +export function parseSlashInvocation(body: string): SlashInvocation | null { + const match = body.match(/^\/([a-z][a-z0-9_-]*)(?:\s+([\s\S]*))?$/i); + if (!match) return null; + return { command: match[1].toLowerCase(), args: match[2] ?? "" }; +} + +export function validateSlashInvocation(slash: SlashInvocation): void { + if (slash.command === "escalate" && slash.args.trim().length === 0) { + throw new Error("Slash command /escalate requires details."); + } + if (slash.command === "approve" || slash.command === "deny") parseApprovalDecisionTarget(slash); + if (slash.command === "board") parseBoardCommand(slash.args); +} + +export function parseApprovalDecisionTarget(slash: SlashInvocation): string { + const approvalId = slash.args.trim().split(/\s+/, 1)[0] ?? ""; + if (!approvalId) throw new Error(`Slash command /${slash.command} requires an approval id.`); + return approvalId; +} + +export function firstToken(value: string): string { + return value.trim().split(/\s+/, 1)[0] ?? ""; +} + +export function approvalRequester(senderType: SendMessageInput["senderType"], sender: PrincipalRef | null): ApprovalRequester { + if (senderType === "system") return { type: "system", id: null }; + if (!sender) throw new Error("Approval requester is missing."); + if (senderType === "agent") return { type: "agent", id: sender.id }; + return { type: "employee", id: sender.id }; +} + +export function spaceName(db: DatabaseSync, spaceId: string): string { + const row = db.prepare("select name from chat_spaces where id = ?").get(spaceId) as { name: string } | undefined; + if (!row) throw new Error("Chat space was not found."); + return row.name; +} + +export function createThreadTitle(body: string): string { + const firstLine = body.split(/\r?\n/, 1)[0].trim(); + return firstLine.length > 80 ? `${firstLine.slice(0, 77)}...` : firstLine; +} + +export function retentionPolicy(value: string | undefined, visibility: CreateChatSpaceInput["visibility"]): string { + const policy = value ?? (visibility === "restricted" ? "restricted_365d" : "standard_365d"); + if (!RETENTION_POLICIES.has(policy)) throw new Error("Chat retention policy is invalid."); + return policy; +} diff --git a/AgentCompany-New/src/main/services/chat-service.ts b/AgentCompany-New/src/main/services/chat-service.ts index 370d87f0..466d2832 100644 --- a/AgentCompany-New/src/main/services/chat-service.ts +++ b/AgentCompany-New/src/main/services/chat-service.ts @@ -6,12 +6,27 @@ import { FILE_SCAN_STATE, RISK_LEVEL, ROLE_KEY, - type FileScanState, } from "../../shared/domain"; import { createId } from "../../shared/ids"; import { ApprovalService } from "./approval-service"; import { AuditService } from "./audit-service"; -import { BoardInterventionService, parseBoardCommand } from "./board-intervention-service"; +import { BoardInterventionService } from "./board-intervention-service"; +import { + ALLOWED_SLASH_COMMANDS, + approvalRequester, + createThreadTitle, + firstToken, + parseApprovalDecisionTarget, + parseSlashInvocation, + retentionPolicy, + spaceName, + validateSlashInvocation, + type FileAttachmentRow, + type PrincipalRef, + type SlashInvocation, + type SpaceRow, + type ThreadRow, +} from "./chat-service-internal"; import { IdentityAccessService } from "./identity-access-service"; import { buildChatMessageStructure } from "./chat-message-structure"; import { ChatMessageMutationService } from "./chat-message-mutation-service"; @@ -28,36 +43,6 @@ const SPACE_TYPES = new Set([ "direct", ]); const SPACE_VISIBILITIES = new Set(["workspace", "restricted"]); -const RETENTION_POLICIES = new Set(["standard_365d", "restricted_365d"]); - -interface FileAttachmentRow { - id: string; - sha256: string; - scan_state: FileScanState; - scan_labels_json: string; - storage_path: string; - owner_type: string; - owner_id: string | null; -} - -interface SpaceRow { - id: string; - org_id: string; - visibility: string; - space_type: string; -} - -interface ThreadRow { - id: string; - board_state: string; - controlled_by_type: "employee" | "agent" | "board" | null; - controlled_by_id: string | null; -} - -interface PrincipalRef { - type: "employee" | "agent"; - id: string; -} export class ChatService { private readonly audit: AuditService; @@ -512,73 +497,3 @@ export class ChatService { `).run(spaceId, principal.type, principal.id, role, "unmuted"); } } - -interface SlashInvocation { - command: string; - args: string; -} - -interface ApprovalRequester { - type: "employee" | "agent" | "system"; - id: string | null; -} - -const ALLOWED_SLASH_COMMANDS = new Set([ - "agent", - "handoff", - "approve", - "deny", - "skill", - "runbook", - "file", - "escalate", - "board", -]); - -function parseSlashInvocation(body: string): SlashInvocation | null { - const match = body.match(/^\/([a-z][a-z0-9_-]*)(?:\s+([\s\S]*))?$/i); - if (!match) return null; - return { command: match[1].toLowerCase(), args: match[2] ?? "" }; -} - -function validateSlashInvocation(slash: SlashInvocation): void { - if (slash.command === "escalate" && slash.args.trim().length === 0) { - throw new Error("Slash command /escalate requires details."); - } - if (slash.command === "approve" || slash.command === "deny") parseApprovalDecisionTarget(slash); - if (slash.command === "board") parseBoardCommand(slash.args); -} - -function parseApprovalDecisionTarget(slash: SlashInvocation): string { - const approvalId = slash.args.trim().split(/\s+/, 1)[0] ?? ""; - if (!approvalId) throw new Error(`Slash command /${slash.command} requires an approval id.`); - return approvalId; -} - -function firstToken(value: string): string { - return value.trim().split(/\s+/, 1)[0] ?? ""; -} - -function approvalRequester(senderType: SendMessageInput["senderType"], sender: PrincipalRef | null): ApprovalRequester { - if (senderType === "system") return { type: "system", id: null }; - if (!sender) throw new Error("Approval requester is missing."); - if (senderType === "agent") return { type: "agent", id: sender.id }; - return { type: "employee", id: sender.id }; -} - -function spaceName(db: DatabaseSync, spaceId: string): string { - const row = db.prepare("select name from chat_spaces where id = ?").get(spaceId) as { name: string } | undefined; - if (!row) throw new Error("Chat space was not found."); - return row.name; -} - -function createThreadTitle(body: string): string { - const firstLine = body.split(/\r?\n/, 1)[0].trim(); - return firstLine.length > 80 ? `${firstLine.slice(0, 77)}...` : firstLine; -} - -function retentionPolicy(value: string | undefined, visibility: CreateChatSpaceInput["visibility"]): string { - const policy = value ?? (visibility === "restricted" ? "restricted_365d" : "standard_365d"); - if (!RETENTION_POLICIES.has(policy)) throw new Error("Chat retention policy is invalid."); - return policy; -}