Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/relay-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ jobs:
run: |
echo "Selected CLI platforms: ${{ needs.prepare-matrix.outputs.selected_platforms }}"

- name: Install Linux FUSE build dependencies
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq libfuse-dev libfuse2t64

- name: Prepare shared Node runtime
run: node relay/scripts/prepare-node-bundle.mjs --target-platform=${{ matrix.goos }}-${{ matrix.goarch }} --node-version=${{ env.BUNDLED_NODE_VERSION }}

Expand All @@ -146,6 +152,23 @@ jobs:
go build -ldflags="-s -w -X main.Version=${{ steps.version.outputs.version }}" \
-o ../synapse-relay-${{ matrix.suffix }}${{ matrix.ext }} ./cmd/synapse-relay

- name: Run Relay VFS unit/build validation
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
working-directory: relay
run: |
bash scripts/validate-vfs-unit.sh

- name: Build Linux mount helper
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
working-directory: relay
env:
CGO_ENABLED: "1"
GOOS: linux
GOARCH: amd64
run: |
go build -tags 'relay_fuse,desktop_cua' -ldflags="-s -w -X main.Version=${{ steps.version.outputs.version }}" \
-o ../synapse-relay-mount-linux-amd64 ./cmd/synapse-relay-mount

- name: Upload artifacts to Tencent COS
if: startsWith(github.ref, 'refs/tags/') || env.ENABLE_COS_UPLOAD == 'true'
continue-on-error: true
Expand Down Expand Up @@ -245,6 +268,14 @@ jobs:
name: synapse-relay-${{ matrix.suffix }}
path: synapse-relay-${{ matrix.suffix }}${{ matrix.ext }}

- name: Upload Linux mount helper artifact
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
continue-on-error: true
uses: actions/upload-artifact@v4
with:
name: synapse-relay-mount-linux-amd64
path: synapse-relay-mount-linux-amd64

release:
needs: build
runs-on: ubuntu-latest
Expand Down
31 changes: 31 additions & 0 deletions .github/workflows/relay-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,12 @@ jobs:
run: |
tar -xzf relay-runtime-bundle-${{ matrix.suffix }}.tar.gz

- name: Install Linux FUSE build dependencies
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq libfuse-dev libfuse2t64

- name: Build
working-directory: relay
env:
Expand All @@ -230,6 +236,23 @@ jobs:
go build -ldflags="-s -w -X main.Version=${{ steps.version.outputs.version }}" \
-o ../synapse-relay-${{ matrix.suffix }}${{ matrix.ext }} ./cmd/synapse-relay

- name: Run Relay VFS unit/build validation
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
working-directory: relay
run: |
bash scripts/validate-vfs-unit.sh

- name: Build Linux mount helper
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
working-directory: relay
env:
CGO_ENABLED: "1"
GOOS: linux
GOARCH: amd64
run: |
go build -tags 'relay_fuse,desktop_cua' -ldflags="-s -w -X main.Version=${{ steps.version.outputs.version }}" \
-o ../synapse-relay-mount-linux-amd64 ./cmd/synapse-relay-mount

- name: Upload artifacts to Tencent COS
if: startsWith(github.ref, 'refs/tags/') || env.ENABLE_COS_UPLOAD == 'true'
continue-on-error: true
Expand Down Expand Up @@ -329,6 +352,14 @@ jobs:
name: synapse-relay-${{ matrix.suffix }}
path: synapse-relay-${{ matrix.suffix }}${{ matrix.ext }}

- name: Upload Linux mount helper artifact
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
continue-on-error: true
uses: actions/upload-artifact@v4
with:
name: synapse-relay-mount-linux-amd64
path: synapse-relay-mount-linux-amd64

build-gui:
needs:
- prepare-gui-matrix
Expand Down
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
当前系统处于初期设计实现阶段,不要考虑兼容旧数据。

## 业务枚举与分支判断规范

- 跨 `packages/api`、`packages/web-next`、`packages/mobile-app` 共享的业务枚举与协议值,统一定义在 `packages/shared`,禁止在消费端重复声明同义字符串联合或手抄枚举数组。
- 业务分支判断禁止直接写原始业务字符串字面量,例如 `participant.type === "actor"`、`status === "pending_approval"`、`z.enum(["workspace_open", "approval_required"])` 这类写法不再允许。
- 统一写法为 shared 常量成员比较,例如 `participant.type === CONVERSATION_PARTICIPANT_TYPE.ACTOR`,以及 `z.enum(RELATIONSHIP_ACCESS_POLICIES)`。
- 共享业务枚举统一使用 `const object + values tuple + 派生 type` 模式;不要只写裸联合类型。
- 数据库已有 enum 的业务值,以 `packages/shared` 为应用层真源,`packages/api/src/infrastructure/database/enum-compat.ts` 负责与 DB 生成类型做编译期对齐。
- 纯 UI 文案、临时交互状态、展示 label、样式 key 不纳入这一条;但文案选择逻辑里涉及业务枚举时,仍必须使用 shared 常量。

## Backend 更新与重启

- 修改 `packages/api` 或任何会影响后端运行时行为、配置加载、数据库访问、权限逻辑的代码后,需要通过 `systemctl` 重启后端服务,不能只假设热更新或旧进程会自动生效。
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"build:api": "npm run build -w packages/api",
"build:daemon": "npm run build -w packages/remote-agent-daemon",
"build:web": "npm run build -w packages/shared && npm run build -w packages/web-next",
"audit:business-enums": "node ./scripts/audit-business-enum-literals.mjs",
"db:bootstrap": "npm run db:bootstrap -w packages/api",
"db:codegen": "npm run db:codegen -w packages/api",
"db:seed": "npm run db:seed -w packages/api",
Expand Down
1 change: 0 additions & 1 deletion packages/api/src/infrastructure/database/generated/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,6 @@ export type RemoteAgentMachinesTrustStatus =
| "revoked"

export type RemoteAgentMessageDeliveriesStatus =
| "acked"
| "completed"
| "failed"
| "pending"
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/infrastructure/database/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ CREATE TYPE remote_agent_machine_sessions_transport AS ENUM ('websocket');
CREATE TYPE remote_agent_bindings_status AS ENUM ('active', 'disabled', 'error');
CREATE TYPE remote_agent_bindings_runtime_state AS ENUM ('offline', 'idle', 'running', 'waiting_user_input', 'plan_drafting', 'waiting_plan_approval', 'error');
CREATE TYPE remote_agent_runs_status AS ENUM ('queued', 'running', 'completed', 'failed', 'cancelled');
CREATE TYPE remote_agent_message_deliveries_status AS ENUM ('pending', 'acked', 'completed', 'failed');
CREATE TYPE remote_agent_message_deliveries_status AS ENUM ('pending', 'completed', 'failed');
CREATE TYPE remote_agent_runtime_catalog_status AS ENUM ('available', 'missing_binary', 'broken_path', 'unsupported_platform', 'runtime_error');
CREATE TYPE relationship_approval_mode AS ENUM ('auto', 'manual');
CREATE TYPE relationship_request_status AS ENUM ('pending', 'approved', 'rejected');
Expand Down
9 changes: 7 additions & 2 deletions packages/api/src/modules/ai/context-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import type {
ProviderContextManifest,
ProviderContextWindow,
} from "@synapse/shared"
import { buildConversationMessageRef, textBlock } from "@synapse/shared"
import {
CONVERSATION_PARTICIPANT_TYPE,
buildConversationMessageRef,
textBlock,
} from "@synapse/shared"
import type {
CanonicalContextItem,
ConversationEntityRef,
Expand Down Expand Up @@ -371,7 +375,8 @@ function compileManifestMessage(
const isSelf =
(manifest.selfParticipantId &&
participant.participantId === manifest.selfParticipantId) ||
(participant.type === "actor" && participant.id === manifest.selfActorId)
(participant.type === CONVERSATION_PARTICIPANT_TYPE.ACTOR &&
participant.id === manifest.selfActorId)
content.push(
toXmlTextBlock(
selfClosingXmlTag("participant", {
Expand Down
12 changes: 8 additions & 4 deletions packages/api/src/modules/ai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
NormalizedMcpToolResult,
} from "@synapse/shared/types"
import {
CONVERSATION_PARTICIPANT_TYPE,
extractText,
formatMentionText,
getDefaultModelEngineKind,
Expand Down Expand Up @@ -344,9 +345,9 @@ async function loadToolResolveConversationParticipants(params: {
type: "actor",
id: member.actor_id,
participantId: member.id,
name: member.actor_name || "Unknown actor",
title: member.actor_title || member.actor_role || "Actor",
role: member.actor_role || undefined,
name: member.participant_name || "Unknown actor",
title: member.participant_title || member.participant_role || "Actor",
role: member.participant_role || undefined,
})
continue
}
Expand Down Expand Up @@ -678,7 +679,10 @@ export async function actorThink(
otherParticipantCount:
currentToolConversationParticipants?.filter(
(participant) =>
!(participant.type === "actor" && participant.id === actor.id)
!(
participant.type === CONVERSATION_PARTICIPANT_TYPE.ACTOR &&
participant.id === actor.id
)
).length || 0,
})
const buildResolveCtx = (): ToolResolveContext => ({
Expand Down
53 changes: 36 additions & 17 deletions packages/api/src/modules/ai/inline-ref-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
CONVERSATION_PARTICIPANT_TYPE,
mentionBlock,
textBlock,
type CanonicalContentBlock,
Expand Down Expand Up @@ -125,27 +126,41 @@ function uniqueMatch(

function normalizeMentionType(
value: string
): "actor" | "workspace_member" | "external" | null {
):
| typeof CONVERSATION_PARTICIPANT_TYPE.ACTOR
| typeof CONVERSATION_PARTICIPANT_TYPE.WORKSPACE_MEMBER
| typeof CONVERSATION_PARTICIPANT_TYPE.EXTERNAL
| null {
const normalized = value.trim().toLowerCase()
if (normalized === "actor") return "actor"
if (normalized === "user" || normalized === "workspace_member") {
return "workspace_member"
if (normalized === CONVERSATION_PARTICIPANT_TYPE.ACTOR) {
return CONVERSATION_PARTICIPANT_TYPE.ACTOR
}
if (
normalized === "user" ||
normalized === CONVERSATION_PARTICIPANT_TYPE.WORKSPACE_MEMBER
) {
return CONVERSATION_PARTICIPANT_TYPE.WORKSPACE_MEMBER
}
if (normalized === CONVERSATION_PARTICIPANT_TYPE.EXTERNAL) {
return CONVERSATION_PARTICIPANT_TYPE.EXTERNAL
}
if (normalized === "external") return "external"
return null
}

function matchesMentionTypeAndId(
candidate: ConversationEntityRef,
participantType: "actor" | "workspace_member" | "external",
participantType:
| typeof CONVERSATION_PARTICIPANT_TYPE.ACTOR
| typeof CONVERSATION_PARTICIPANT_TYPE.WORKSPACE_MEMBER
| typeof CONVERSATION_PARTICIPANT_TYPE.EXTERNAL,
id: string
): boolean {
if (candidate.participantType !== participantType) return false

if (participantType === "actor") {
if (participantType === CONVERSATION_PARTICIPANT_TYPE.ACTOR) {
return candidate.actorId === id || candidate.participantId === id
}
if (participantType === "workspace_member") {
if (participantType === CONVERSATION_PARTICIPANT_TYPE.WORKSPACE_MEMBER) {
return candidate.workspaceMemberId === id || candidate.participantId === id
}
return (
Expand All @@ -156,10 +171,12 @@ function matchesMentionTypeAndId(
}

function preferredMentionId(candidate: ConversationEntityRef): string | null {
if (candidate.participantType === "actor") {
if (candidate.participantType === CONVERSATION_PARTICIPANT_TYPE.ACTOR) {
return candidate.actorId || candidate.participantId || null
}
if (candidate.participantType === "workspace_member") {
if (
candidate.participantType === CONVERSATION_PARTICIPANT_TYPE.WORKSPACE_MEMBER
) {
return candidate.workspaceMemberId || candidate.participantId || null
}
return (
Expand Down Expand Up @@ -231,7 +248,9 @@ function resolveMentionFromName(
if (GENERIC_USER_KEYS.has(normalized)) {
if (options.defaultUser) return { mention: options.defaultUser }
const userMatches = candidates.filter(
(candidate) => candidate.participantType === "workspace_member"
(candidate) =>
candidate.participantType ===
CONVERSATION_PARTICIPANT_TYPE.WORKSPACE_MEMBER
)
const match = uniqueMatch(userMatches)
if (match) {
Expand Down Expand Up @@ -446,9 +465,9 @@ export async function resolveInlineReferenceSegments(
export function conversationParticipantEntryToEntityRef(
participant: ConversationParticipantEntry
): ConversationEntityRef {
if (participant.type === "actor") {
if (participant.type === CONVERSATION_PARTICIPANT_TYPE.ACTOR) {
return {
participantType: "actor",
participantType: CONVERSATION_PARTICIPANT_TYPE.ACTOR,
actorId: participant.id,
participantId: participant.participantId,
name: participant.name,
Expand All @@ -457,9 +476,9 @@ export function conversationParticipantEntryToEntityRef(
}
}

if (participant.type === "workspace_member") {
if (participant.type === CONVERSATION_PARTICIPANT_TYPE.WORKSPACE_MEMBER) {
return {
participantType: "workspace_member",
participantType: CONVERSATION_PARTICIPANT_TYPE.WORKSPACE_MEMBER,
workspaceMemberId: participant.id,
participantId: participant.participantId,
name: participant.name,
Expand All @@ -469,7 +488,7 @@ export function conversationParticipantEntryToEntityRef(
}

return {
participantType: "external",
participantType: CONVERSATION_PARTICIPANT_TYPE.EXTERNAL,
workspaceMemberId: participant.linkedWorkspaceMemberId,
participantId: participant.participantId,
externalUserKey: participant.externalUserKey,
Expand All @@ -485,7 +504,7 @@ export function buildDefaultUserMention(params: {
}): ConversationEntityRef | undefined {
if (!params.workspaceMemberId) return undefined
return {
participantType: "workspace_member",
participantType: CONVERSATION_PARTICIPANT_TYPE.WORKSPACE_MEMBER,
workspaceMemberId: params.workspaceMemberId,
name: params.userName || "User",
}
Expand Down
4 changes: 3 additions & 1 deletion packages/api/src/modules/ai/prompt-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
isPlanCollaborationMode,
isPlanDraftingCollaborationMode,
} from "@synapse/shared/utils"
import { sortAvailableSkillsForDiscovery } from "../skills/discovery-order.js"
import { buildReplyToRefUsageGuidance } from "./session-tool-guidance.js"

export interface ConversationParticipantInfo {
Expand Down Expand Up @@ -388,11 +389,12 @@ export function buildActorPrompt(
)

if (availableSkills && availableSkills.length > 0) {
const orderedSkills = sortAvailableSkillsForDiscovery(availableSkills)
parts.push(
`# Available Skills\n` +
`These skills are available on demand. Do not assume their detailed contents are already loaded.\n` +
`If one skill clearly matches the task, call \`read_skill\` to read its description or a referenced attachment before using it.\n` +
availableSkills
orderedSkills
.map(
(skill) =>
`- \`${skill.slug}\`${skill.sourceKind === "relay_auto_loaded" ? " (relay auto-loaded)" : ""}: ${skill.description}`
Expand Down
21 changes: 16 additions & 5 deletions packages/api/src/modules/ai/session-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import {
import { buildNormalizedMessageContent } from "../chat/message-content.js"
import { buildDefaultUserMention } from "./inline-ref-resolver.js"
import { runMemorySearch } from "../memory/service.js"
import { sortAvailableSkillsForDiscovery } from "../skills/discovery-order.js"
import { readVisibleSkill } from "../skills/service.js"
import {
listAutomationEventSources,
Expand Down Expand Up @@ -1140,21 +1141,31 @@ export function registerCallableToolPlugins(): void {
},
},
resolve: (ctx) => {
const availableSkills = ctx.availableSkills || []
const availableSkills = sortAvailableSkillsForDiscovery(
ctx.availableSkills || []
)
if (availableSkills.length === 0) {
return { active: false, definition: null as any }
}
const skillNames: string[] = Array.from(
new Set(availableSkills.map((skill) => skill.slug))
)
const skillList = availableSkills
.map((skill) => `\`${skill.slug}\`: ${skill.description}`)
.join("; ")
const previewSkills = skillNames
.slice(0, 12)
.map((skill) => `\`${skill}\``)
const moreCount = skillNames.length - previewSkills.length
const availabilityHint =
moreCount > 0
? `${previewSkills.join(", ")}, and ${moreCount} more listed in the Available Skills section.`
: `${previewSkills.join(", ")}.`
return {
active: true,
definition: {
name: "read_skill",
description: `Read the contents of an available skill package. Available skills: ${skillList}`,
description:
`Read the contents of an available skill package. ` +
`Use the exact slug from the Available Skills section. ` +
`Currently available: ${availabilityHint}`,
parameters: {
type: "object",
properties: {
Expand Down
Loading
Loading