Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c5cfb6b
feat: add Octopus managed site integration
Hureru Feb 7, 2026
476e955
Merge branch 'qixing-jk:main' into feature/octopus-integration
Hureru Feb 8, 2026
7e53506
fix(types): extend ChannelFormData.type to support OctopusOutboundType
Hureru Feb 8, 2026
fe36521
fix(octopus): trim input values before saving in OctopusSettings hand…
Hureru Feb 8, 2026
a5a4dc3
fix(octopus): hide priority and weight columns when isOctopus is true
Hureru Feb 8, 2026
240038f
fix(octopus): handle non-JSON error responses in login
Hureru Feb 8, 2026
3424a05
fix(octopus): add robust error handling for non-JSON responses in fet…
Hureru Feb 8, 2026
130c88a
fix(octopus): return null instead of envelope when responseData.data …
Hureru Feb 8, 2026
73edf47
fix(octopus): avoid mutating input channel and use typed error handling
Hureru Feb 8, 2026
ff43617
fix(octopus): validate config in listChannels and executeSyncForOctopus
Hureru Feb 8, 2026
6757fd3
refactor(octopus): define explicit OctopusChannelWithData composite type
Hureru Feb 8, 2026
c7e14f4
fix(octopus): add proper ChannelType to OctopusOutboundType mapping
Hureru Feb 8, 2026
690f9a6
fix(octopus): normalize accountBaseUrl in findMatchingChannel
Hureru Feb 8, 2026
d0986eb
fix(preferences): add migration v12 to initialize octopus config
Hureru Feb 8, 2026
2e06cca
fix(octopus): address code review issues for type safety and error ha…
Hureru Feb 8, 2026
a19b35d
refactor(octopus): simplify token caching to memory-only
Hureru Feb 8, 2026
8a9ab2f
Merge branch 'main' into feature/octopus-integration
Hureru Feb 8, 2026
6e32fec
refactor: extract shared utils and remove unused octopus functions
Hureru Feb 8, 2026
cf2eca5
feat(octopus): add model/group list APIs and CORS error hint
Hureru Feb 9, 2026
f27f520
fix(octopus): guard base_urls and url with optional chaining in searc…
Hureru Feb 9, 2026
e3adeda
fix(octopus): surface login error details to UI toast
Hureru Feb 11, 2026
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
Binary file added assets/OctopusLogo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
150 changes: 85 additions & 65 deletions components/ChannelDialog/components/ChannelDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ import {
} from "~/components/ui"
import { DIALOG_MODES, type DialogMode } from "~/constants/dialogModes"
import { ChannelType, ChannelTypeOptions } from "~/constants/managedSite"
import { OctopusOutboundTypeOptions } from "~/constants/octopus"
import { OCTOPUS } from "~/constants/siteType"
import { useUserPreferencesContext } from "~/contexts/UserPreferencesContext"
import {
CHANNEL_STATUS,
type ChannelFormData,
type ChannelStatus,
type ManagedSiteChannel,
} from "~/types/managedSite"
import { OctopusOutboundType } from "~/types/octopus"

export interface ChannelDialogProps {
isOpen: boolean
Expand Down Expand Up @@ -61,6 +65,8 @@ export function ChannelDialog({
}: ChannelDialogProps) {
const { t } = useTranslation(["channelDialog", "common"])
const [showKey, setShowKey] = useState(false)
const { managedSiteType } = useUserPreferencesContext()
const isOctopus = managedSiteType === OCTOPUS

const {
formData,
Expand Down Expand Up @@ -182,7 +188,9 @@ export function ChannelDialog({
: String(formData.type)
}
onValueChange={(value) =>
handleTypeChange(Number(value) as ChannelType)
handleTypeChange(
Number(value) as ChannelType | OctopusOutboundType,
)
}
disabled={isSaving || mode === DIALOG_MODES.EDIT}
required
Expand All @@ -193,11 +201,17 @@ export function ChannelDialog({
/>
</SelectTrigger>
<SelectContent>
{ChannelTypeOptions.map((option) => (
<SelectItem key={option.value} value={String(option.value)}>
{option.label}
</SelectItem>
))}
{isOctopus
? OctopusOutboundTypeOptions.map((option) => (
<SelectItem key={option.value} value={String(option.value)}>
{option.label}
</SelectItem>
))
: ChannelTypeOptions.map((option) => (
<SelectItem key={option.value} value={String(option.value)}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="dark:text-dark-text-secondary mt-1 text-xs text-gray-500">
Expand Down Expand Up @@ -316,73 +330,79 @@ export function ChannelDialog({
</p>
</div>

{/* Groups */}
<div>
<CompactMultiSelect
label={t("channelDialog:fields.groups.label")}
options={availableGroups}
selected={formData.groups}
onChange={(groups) => updateField("groups", groups)}
placeholder={
isLoadingGroups
? t("channelDialog:fields.groups.loading")
: t("channelDialog:fields.groups.placeholder")
}
disabled={isSaving || isLoadingGroups}
allowCustom
/>
<p className="dark:text-dark-text-secondary mt-1 text-xs text-gray-500">
{t("channelDialog:fields.groups.hint")}
</p>
</div>
{/* Groups - Octopus 没有分组概念,隐藏此字段 */}
{!isOctopus && (
<div>
<CompactMultiSelect
label={t("channelDialog:fields.groups.label")}
options={availableGroups}
selected={formData.groups}
onChange={(groups) => updateField("groups", groups)}
placeholder={
isLoadingGroups
? t("channelDialog:fields.groups.loading")
: t("channelDialog:fields.groups.placeholder")
}
disabled={isSaving || isLoadingGroups}
allowCustom
/>
<p className="dark:text-dark-text-secondary mt-1 text-xs text-gray-500">
{t("channelDialog:fields.groups.hint")}
</p>
</div>
)}

{/* Advanced Settings */}
<details className="dark:border-dark-bg-tertiary rounded-lg border border-gray-200 p-3">
<summary className="dark:text-dark-text-primary cursor-pointer text-sm font-medium text-gray-700">
{t("channelDialog:sections.advanced")}
</summary>
<div className="mt-3 space-y-4">
{/* Priority */}
<div>
<Label htmlFor="channel-priority">
{t("channelDialog:fields.priority.label")}
</Label>
<Input
id="channel-priority"
type="number"
value={formData.priority}
onChange={(e) =>
updateField("priority", parseInt(e.target.value) || 0)
}
placeholder="0"
disabled={isSaving}
min="0"
/>
<p className="dark:text-dark-text-secondary mt-1 text-xs text-gray-500">
{t("channelDialog:fields.priority.hint")}
</p>
</div>
{/* Priority - Octopus 不支持优先级 */}
{!isOctopus && (
<div>
<Label htmlFor="channel-priority">
{t("channelDialog:fields.priority.label")}
</Label>
<Input
id="channel-priority"
type="number"
value={formData.priority}
onChange={(e) =>
updateField("priority", parseInt(e.target.value) || 0)
}
placeholder="0"
disabled={isSaving}
min="0"
/>
<p className="dark:text-dark-text-secondary mt-1 text-xs text-gray-500">
{t("channelDialog:fields.priority.hint")}
</p>
</div>
)}

{/* Weight */}
<div>
<Label htmlFor="channel-weight">
{t("channelDialog:fields.weight.label")}
</Label>
<Input
id="channel-weight"
type="number"
value={formData.weight}
onChange={(e) =>
updateField("weight", parseInt(e.target.value) || 0)
}
placeholder="0"
disabled={isSaving}
min="0"
/>
<p className="dark:text-dark-text-secondary mt-1 text-xs text-gray-500">
{t("channelDialog:fields.weight.hint")}
</p>
</div>
{/* Weight - Octopus 不支持权重 */}
{!isOctopus && (
<div>
<Label htmlFor="channel-weight">
{t("channelDialog:fields.weight.label")}
</Label>
<Input
id="channel-weight"
type="number"
value={formData.weight}
onChange={(e) =>
updateField("weight", parseInt(e.target.value) || 0)
}
placeholder="0"
disabled={isSaving}
min="0"
/>
<p className="dark:text-dark-text-secondary mt-1 text-xs text-gray-500">
{t("channelDialog:fields.weight.hint")}
</p>
</div>
)}

{/* Status */}
<div>
Expand Down
69 changes: 69 additions & 0 deletions components/ChannelDialog/components/OctopusTypeSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useTranslation } from "react-i18next"

import {
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui"
import { OctopusOutboundTypeOptions } from "~/constants/octopus"
import { OctopusOutboundType } from "~/types/octopus"

export interface OctopusTypeSelectorProps {
value: OctopusOutboundType | undefined | null
onChange: (value: OctopusOutboundType) => void
disabled?: boolean
required?: boolean
}

/**
* Type selector specifically for Octopus channels.
* Displays only the 6 Octopus outbound types instead of 55+ New API types.
* @param props Component props
* @param props.value Current selected type value
* @param props.onChange Callback when type is changed
* @param props.disabled Whether the selector is disabled
* @param props.required Whether the field is required
*/
export function OctopusTypeSelector({
value,
onChange,
disabled = false,
required = false,
}: OctopusTypeSelectorProps) {
const { t } = useTranslation(["channelDialog"])

return (
<div>
<Label htmlFor="channel-type" required={required}>
{t("channelDialog:fields.type.label")}
</Label>
<Select
value={value === undefined || value === null ? "" : String(value)}
onValueChange={(val) => onChange(Number(val) as OctopusOutboundType)}
disabled={disabled}
required={required}
>
<SelectTrigger id="channel-type">
<SelectValue
placeholder={t("channelDialog:fields.type.placeholder")}
/>
</SelectTrigger>
<SelectContent>
{OctopusOutboundTypeOptions.map((option) => (
<SelectItem key={option.value} value={String(option.value)}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="dark:text-dark-text-secondary mt-1 text-xs text-gray-500">
{t("channelDialog:fields.type.hint")}
</p>
</div>
)
}

export default OctopusTypeSelector
17 changes: 8 additions & 9 deletions components/ChannelDialog/hooks/useChannelForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
ManagedSiteChannel,
UpdateChannelPayload,
} from "~/types/managedSite"
import type { OctopusOutboundType } from "~/types/octopus"
import { createLogger } from "~/utils/logger"
import { mergeUniqueOptions } from "~/utils/selectOptions"

Expand Down Expand Up @@ -85,14 +86,12 @@ export function useChannelForm({
const [isSaving, setIsSaving] = useState(false)
const [isLoadingGroups, setIsLoadingGroups] = useState(false)
const [isLoadingModels, setIsLoadingModels] = useState(false)
const [availableGroups, setAvailableGroups] =
useState<CompactMultiSelectOption[]>(
[],
)
const [availableModels, setAvailableModels] =
useState<CompactMultiSelectOption[]>(
[],
)
const [availableGroups, setAvailableGroups] = useState<
CompactMultiSelectOption[]
>([])
const [availableModels, setAvailableModels] = useState<
CompactMultiSelectOption[]
>([])

// Load groups and model suggestions on mount
useEffect(() => {
Expand Down Expand Up @@ -231,7 +230,7 @@ export function useChannelForm({
setFormData((prev) => ({ ...prev, [field]: value }))
}

const handleTypeChange = (newType: ChannelType) => {
const handleTypeChange = (newType: ChannelType | OctopusOutboundType) => {
setFormData((prev) => ({
...prev,
type: newType,
Expand Down
4 changes: 2 additions & 2 deletions components/KiloCodeExportDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -572,8 +572,8 @@ export function KiloCodeExportDialog({
const selectedTokenIds = selectedTokenIdsBySite[siteId] ?? []
const tokenOptions: CompactMultiSelectOption[] = inventory.tokens.map(
(token) => ({
value: `${token.id}`,
label: getTokenLabel(token, t("common:labels.token")),
value: `${token.id}`,
label: getTokenLabel(token, t("common:labels.token")),
}),
)

Expand Down
7 changes: 6 additions & 1 deletion components/icons/ManagedSiteIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { NewAPI } from "@lobehub/icons"

import { ICON_SIZE_CLASSNAME, IconSize } from "~/components/icons/iconSizes"
import { OctopusIcon } from "~/components/icons/OctopusIcon"
import { VeloeraIcon } from "~/components/icons/VeloeraIcon"
import type { ManagedSiteType } from "~/constants/siteType"
import { VELOERA } from "~/constants/siteType"
import { OCTOPUS, VELOERA } from "~/constants/siteType"
import { cn } from "~/lib/utils"

interface ManagedSiteIconProps {
Expand All @@ -18,6 +19,10 @@ export function ManagedSiteIcon({
siteType,
size = "sm",
}: ManagedSiteIconProps) {
if (siteType === OCTOPUS) {
return <OctopusIcon size={size} />
}

if (siteType === VELOERA) {
return <VeloeraIcon size={size} />
}
Expand Down
26 changes: 26 additions & 0 deletions components/icons/OctopusIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import OctopusLogo from "~/assets/OctopusLogo.png"
import { ICON_SIZE_CLASSNAME, IconSize } from "~/components/icons/iconSizes"
import { cn } from "~/lib/utils"

interface OctopusIconProps {
size?: IconSize
className?: string
}

/**
* Octopus icon for the Octopus managed site type.
* Uses the official Octopus logo.
*/
export function OctopusIcon({ size = "sm", className }: OctopusIconProps) {
return (
<img
src={OctopusLogo}
alt="Octopus"
className={cn(
ICON_SIZE_CLASSNAME[size],
"inline-flex items-center justify-center object-contain",
className,
)}
/>
)
}
Loading